diff --git a/backend/components.d.ts b/backend/components.d.ts index 96bb377..6fa544f 100644 --- a/backend/components.d.ts +++ b/backend/components.d.ts @@ -59,6 +59,8 @@ declare module 'vue' { ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] + ElStep: typeof import('element-plus/es')['ElStep'] + ElSteps: typeof import('element-plus/es')['ElSteps'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] diff --git a/backend/src/api/accountSet.js b/backend/src/api/accountSet.js new file mode 100644 index 0000000..9d1740b --- /dev/null +++ b/backend/src/api/accountSet.js @@ -0,0 +1,47 @@ +import request from '@/utils/request' + +export function getAccountSetList(params) { + return request({ + url: '/backend/erp/account-set/list', + method: 'get', + params + }) +} + +export function getAccountSetDetail(id) { + return request({ + url: `/backend/erp/account-set/${id}`, + method: 'get' + }) +} + +export function createAccountSet(data) { + return request({ + url: '/backend/erp/account-set', + method: 'post', + data + }) +} + +export function updateAccountSet(id, data) { + return request({ + url: `/backend/erp/account-set/${id}`, + method: 'put', + data + }) +} + +export function deleteAccountSet(id) { + return request({ + url: `/backend/erp/account-set/${id}`, + method: 'delete' + }) +} + +export function toggleAccountSetStatus(id, status) { + return request({ + url: `/backend/erp/account-set/${id}/status`, + method: 'post', + data: { status } + }) +} diff --git a/backend/src/api/companyContact.js b/backend/src/api/companyContact.js new file mode 100644 index 0000000..9845f7f --- /dev/null +++ b/backend/src/api/companyContact.js @@ -0,0 +1,36 @@ +import request from '@/utils/request' + +// 获取公司联系人列表 +export function getCompanyContactList(params) { + return request({ + url: '/backend/erp/company-contact/list', + method: 'get', + params + }) +} + +// 新增联系人 +export function createCompanyContact(data) { + return request({ + url: '/backend/erp/company-contact', + method: 'post', + data + }) +} + +// 更新联系人 +export function updateCompanyContact(id, data) { + return request({ + url: `/backend/erp/company-contact/${id}`, + method: 'put', + data + }) +} + +// 删除联系人 +export function deleteCompanyContact(id) { + return request({ + url: `/backend/erp/company-contact/${id}`, + method: 'delete' + }) +} diff --git a/backend/src/api/customer.js b/backend/src/api/customer.js new file mode 100644 index 0000000..7412e24 --- /dev/null +++ b/backend/src/api/customer.js @@ -0,0 +1,47 @@ +import request from '@/utils/request' + +export function getCustomerList(params) { + return request({ + url: '/backend/erp/customer/list', + method: 'get', + params + }) +} + +export function getCustomerDetail(id) { + return request({ + url: `/backend/erp/customer/${id}`, + method: 'get' + }) +} + +export function createCustomer(data) { + return request({ + url: '/backend/erp/customer', + method: 'post', + data + }) +} + +export function updateCustomer(id, data) { + return request({ + url: `/backend/erp/customer/${id}`, + method: 'put', + data + }) +} + +export function deleteCustomer(id) { + return request({ + url: `/backend/erp/customer/${id}`, + method: 'delete' + }) +} + +export function toggleCustomerStatus(id, status) { + return request({ + url: `/backend/erp/customer/${id}/status`, + method: 'post', + data: { status } + }) +} diff --git a/backend/src/api/normalSetting.js b/backend/src/api/normalSetting.js new file mode 100644 index 0000000..cde06fe --- /dev/null +++ b/backend/src/api/normalSetting.js @@ -0,0 +1,23 @@ +import request from '@/utils/request' + +export function getNormalSettings() { + return request({ + url: '/backend/erp/normal-settings', + method: 'get' + }) +} + +export function saveNormalSettings(data) { + return request({ + url: '/backend/erp/normal-settings', + method: 'put', + data + }) +} + +export function resetNormalSettings() { + return request({ + url: '/backend/erp/normal-settings/reset', + method: 'post' + }) +} diff --git a/backend/src/api/notifyConfig.js b/backend/src/api/notifyConfig.js new file mode 100644 index 0000000..5b723db --- /dev/null +++ b/backend/src/api/notifyConfig.js @@ -0,0 +1,18 @@ +import request from '@/utils/request' + +// 获取当前用户的通知配置 +export function getNotifyConfig() { + return request({ + url: '/backend/notify-config', + method: 'get' + }) +} + +// 更新当前用户的通知配置 +export function updateNotifyConfig(data) { + return request({ + url: '/backend/notify-config', + method: 'put', + data + }) +} diff --git a/backend/src/api/supplier.js b/backend/src/api/supplier.js new file mode 100644 index 0000000..d9a08a2 --- /dev/null +++ b/backend/src/api/supplier.js @@ -0,0 +1,47 @@ +import request from '@/utils/request' + +export function getSupplierList(params) { + return request({ + url: '/backend/erp/supplier/list', + method: 'get', + params + }) +} + +export function getSupplierDetail(id) { + return request({ + url: `/backend/erp/supplier/${id}`, + method: 'get' + }) +} + +export function createSupplier(data) { + return request({ + url: '/backend/erp/supplier', + method: 'post', + data + }) +} + +export function updateSupplier(id, data) { + return request({ + url: `/backend/erp/supplier/${id}`, + method: 'put', + data + }) +} + +export function deleteSupplier(id) { + return request({ + url: `/backend/erp/supplier/${id}`, + method: 'delete' + }) +} + +export function toggleSupplierStatus(id, status) { + return request({ + url: `/backend/erp/supplier/${id}/status`, + method: 'post', + data: { status } + }) +} diff --git a/backend/src/router/index.js b/backend/src/router/index.js index 1e23ac2..83376f3 100644 --- a/backend/src/router/index.js +++ b/backend/src/router/index.js @@ -1,256 +1,262 @@ -import { createRouter, createWebHashHistory } from "vue-router"; -import { convertMenusToRoutes } from "./dynamicRoutes"; - -// 静态子路由:需要在 Main 框架内显示的页面 -const staticMainChildren = [ - // 默认首页:访问 / 时直接展示工作台,URL 保持为 / - { - path: "", - name: "Dashboard", - component: () => import("@/views/dashboard/index.vue"), - meta: { requiresAuth: true, title: "工作台" } - }, - // CMS 文章/产品/解决方案是系统内置功能,不依赖数据库菜单配置。 - // 统一结构:index 为列表页,type 为分类页。 - { - path: "/apps/cms/analytics/content", - name: "CmsContentAnalytics", - component: () => import("@/views/apps/cms/analytics/content/index.vue"), - meta: { requiresAuth: true, title: "内容统计", modulePath: "/apps/cms" } - }, - { - path: "/apps/cms/article", - name: "CmsArticles", - component: () => import("@/views/apps/cms/article/index/index.vue"), - meta: { requiresAuth: true, title: "文章管理", modulePath: "/apps/cms" } - }, - { - path: "/apps/cms/article/type", - name: "CmsArticleCategories", - component: () => import("@/views/apps/cms/article/type/index.vue"), - meta: { requiresAuth: true, title: "文章分类", modulePath: "/apps/cms" } - }, - { - path: "/apps/cms/products", - name: "CmsProducts", - component: () => import("@/views/apps/cms/product/index/index.vue"), - meta: { requiresAuth: true, title: "产品管理", modulePath: "/apps/cms" } - }, - { - path: "/apps/cms/products/types", - name: "CmsProductCategories", - component: () => import("@/views/apps/cms/product/type/index.vue"), - meta: { requiresAuth: true, title: "产品分类", modulePath: "/apps/cms" } - }, - { - path: "/apps/cms/solutions", - name: "CmsSolutions", - component: () => import("@/views/apps/cms/solution/index/index.vue"), - meta: { requiresAuth: true, title: "解决方案管理", modulePath: "/apps/cms" } - }, - { - path: "/apps/cms/solutions/types", - name: "CmsSolutionCategories", - component: () => import("@/views/apps/cms/solution/type/index.vue"), - meta: { requiresAuth: true, title: "解决方案分类", modulePath: "/apps/cms" } - }, - // 兼容旧路径:articles/* -> article/* - { - path: "/apps/cms/articles", - redirect: "/apps/cms/article" - }, - { - path: "/apps/cms/articles/category", - redirect: "/apps/cms/article/type" - }, - { - path: "/user/userProfile", - name: "userProfile", - component: () => import("@/views/user/userProfile.vue"), - meta: { requiresAuth: true, title: "用户中心" } - }, - // 兼容拼写错误的路径重定向 - { - path: "/apps/erp/dashborad", - redirect: "/apps/erp/dashboard" - } -]; - -// 静态路由:登录页独立、home 导航门户独立、404 页面独立 -const staticRoutes = [ - { - path: "/login", - name: "Login", - component: () => import("@/views/login/index.vue"), - meta: { requiresAuth: false } - }, - { - path: "/register", - name: "Register", - component: () => import("@/views/login/register.vue"), - meta: { requiresAuth: false } - }, - { - path: "/forget", - name: "ForgetPassword", - component: () => import("@/views/login/forget.vue"), - meta: { requiresAuth: false } - }, - { - path: "/home", - name: "Home", - component: () => import("@/views/home/index.vue"), - meta: { requiresAuth: true, title: "系统导航", isStandalone: true } - }, - // 兼容路径拼写错误:dashborad -> dashboard - { - path: "/apps/erp/dashborad", - redirect: "/apps/erp/dashboard" - }, - { - path: "/:pathMatch(.*)*", - name: "NotFound", - component: () => import("@/views/404/404.vue"), - meta: { requiresAuth: false } - } -]; - -const router = createRouter({ - history: createWebHashHistory(), - routes: staticRoutes -}); - -let dynamicRoutesAdded = false; -let dynamicRoutesData = []; -let routesLoadingPromise = null; - -export function resetDynamicRoutes() { - dynamicRoutesAdded = false; - routesLoadingPromise = null; -} - -export async function loadAndAddDynamicRoutes() { - if (routesLoadingPromise) { - return routesLoadingPromise; - } - - if (dynamicRoutesAdded) { - return Promise.resolve(); - } - - routesLoadingPromise = (async () => { - try { - const { useMenuStore } = await import("@/stores/menu"); - const menuStore = useMenuStore(); - const menuData = await menuStore.fetchMenus(); - - if (menuData && menuData.length > 0) { - addDynamicRoutes(menuData); - dynamicRoutesAdded = true; - routesLoadingPromise = null; - return Promise.resolve(); - } else { - // 即使权限菜单为空,也要注册内置文章中心路由。 - addDynamicRoutes([]); - dynamicRoutesAdded = true; - routesLoadingPromise = null; - return Promise.resolve(); - } - } catch (error) { - console.error('加载动态路由失败:', error); - dynamicRoutesAdded = true; - routesLoadingPromise = null; - throw error; - } - })(); - - return routesLoadingPromise; -} - -// 核心修改:移除扁平化,直接使用嵌套菜单生成路由 -function addDynamicRoutes(menus) { - if (!menus?.length) { - return; - } - - // 直接转换嵌套菜单为嵌套路由(不再扁平化) - const dynamicRoutes = convertMenusToRoutes(menus); - - if (router.hasRoute('Main')) { - router.removeRoute('Main'); - } - - // 重新添加主路由,合并静态子路由和动态路由 - router.addRoute({ - path: "/", - name: "Main", - component: () => import("@/views/Main.vue"), - meta: { requiresAuth: true }, - children: [...staticMainChildren, ...dynamicRoutes] // 合并静态和动态子路由 - }); - - dynamicRoutesAdded = true; -} - -function findRouteByName(routes, routeName) { - for (const route of routes) { - if (route.name === routeName) { - return route; - } - if (route.children) { - const found = findRouteByName(route.children, routeName); - if (found) { - return found; - } - } - } - return null; -} - -function findFirstValidRoute(routes) { - for (const route of routes) { - if (route.component) { - return route; - } - if (route.children && route.children.length > 0) { - const childRoute = findFirstValidRoute(route.children); - if (childRoute) { - return childRoute; - } - } - } - return null; -} - -router.beforeEach(async (to, from, next) => { - const token = localStorage.getItem("token"); - const publicPaths = ["/login", "/register", "/forget"]; - - if (publicPaths.includes(to.path)) { - if (token) { - if (!dynamicRoutesAdded) { - await loadAndAddDynamicRoutes(); - } - next({ path: "/home" }); - } else { - next(); - } - return; - } - - if (!token) { - next({ path: "/login", query: { redirect: to.path } }); - return; - } - - if (!dynamicRoutesAdded) { - await loadAndAddDynamicRoutes(); - // 路由加载后重新导航,确保路由匹配正确 - next({ path: to.path, replace: true }); - return; - } - - next(); -}); - -export default router; +import { createRouter, createWebHashHistory } from "vue-router"; +import { convertMenusToRoutes } from "./dynamicRoutes"; + +// 静态子路由:需要在 Main 框架内显示的页面 +const staticMainChildren = [ + // 默认首页:访问 / 时直接展示工作台,URL 保持为 / + { + path: "", + name: "Dashboard", + component: () => import("@/views/dashboard/index.vue"), + meta: { requiresAuth: true, title: "工作台" } + }, + // CMS 文章/产品/解决方案是系统内置功能,不依赖数据库菜单配置。 + // 统一结构:index 为列表页,type 为分类页。 + { + path: "/apps/cms/analytics/content", + name: "CmsContentAnalytics", + component: () => import("@/views/apps/cms/analytics/content/index.vue"), + meta: { requiresAuth: true, title: "内容统计", modulePath: "/apps/cms" } + }, + { + path: "/apps/cms/article", + name: "CmsArticles", + component: () => import("@/views/apps/cms/article/index/index.vue"), + meta: { requiresAuth: true, title: "文章管理", modulePath: "/apps/cms" } + }, + { + path: "/apps/cms/article/type", + name: "CmsArticleCategories", + component: () => import("@/views/apps/cms/article/type/index.vue"), + meta: { requiresAuth: true, title: "文章分类", modulePath: "/apps/cms" } + }, + { + path: "/apps/cms/products", + name: "CmsProducts", + component: () => import("@/views/apps/cms/product/index/index.vue"), + meta: { requiresAuth: true, title: "产品管理", modulePath: "/apps/cms" } + }, + { + path: "/apps/cms/products/types", + name: "CmsProductCategories", + component: () => import("@/views/apps/cms/product/type/index.vue"), + meta: { requiresAuth: true, title: "产品分类", modulePath: "/apps/cms" } + }, + { + path: "/apps/cms/solutions", + name: "CmsSolutions", + component: () => import("@/views/apps/cms/solution/index/index.vue"), + meta: { requiresAuth: true, title: "解决方案管理", modulePath: "/apps/cms" } + }, + { + path: "/apps/cms/solutions/types", + name: "CmsSolutionCategories", + component: () => import("@/views/apps/cms/solution/type/index.vue"), + meta: { requiresAuth: true, title: "解决方案分类", modulePath: "/apps/cms" } + }, + // 兼容旧路径:articles/* -> article/* + { + path: "/apps/cms/articles", + redirect: "/apps/cms/article" + }, + { + path: "/apps/cms/articles/category", + redirect: "/apps/cms/article/type" + }, + { + path: "/user/userProfile", + name: "userProfile", + component: () => import("@/views/user/userProfile.vue"), + meta: { requiresAuth: true, title: "用户中心" } + }, + { + path: "/user/notifyConfig", + name: "notifyConfig", + component: () => import("@/views/user/notifyConfig.vue"), + meta: { requiresAuth: true, title: "通知设置" } + }, + // 兼容拼写错误的路径重定向 + { + path: "/apps/erp/dashborad", + redirect: "/apps/erp/dashboard" + } +]; + +// 静态路由:登录页独立、home 导航门户独立、404 页面独立 +const staticRoutes = [ + { + path: "/login", + name: "Login", + component: () => import("@/views/login/index.vue"), + meta: { requiresAuth: false } + }, + { + path: "/register", + name: "Register", + component: () => import("@/views/login/register.vue"), + meta: { requiresAuth: false } + }, + { + path: "/forget", + name: "ForgetPassword", + component: () => import("@/views/login/forget.vue"), + meta: { requiresAuth: false } + }, + { + path: "/home", + name: "Home", + component: () => import("@/views/home/index.vue"), + meta: { requiresAuth: true, title: "系统导航", isStandalone: true } + }, + // 兼容路径拼写错误:dashborad -> dashboard + { + path: "/apps/erp/dashborad", + redirect: "/apps/erp/dashboard" + }, + { + path: "/:pathMatch(.*)*", + name: "NotFound", + component: () => import("@/views/404/404.vue"), + meta: { requiresAuth: false } + } +]; + +const router = createRouter({ + history: createWebHashHistory(), + routes: staticRoutes +}); + +let dynamicRoutesAdded = false; +let dynamicRoutesData = []; +let routesLoadingPromise = null; + +export function resetDynamicRoutes() { + dynamicRoutesAdded = false; + routesLoadingPromise = null; +} + +export async function loadAndAddDynamicRoutes() { + if (routesLoadingPromise) { + return routesLoadingPromise; + } + + if (dynamicRoutesAdded) { + return Promise.resolve(); + } + + routesLoadingPromise = (async () => { + try { + const { useMenuStore } = await import("@/stores/menu"); + const menuStore = useMenuStore(); + const menuData = await menuStore.fetchMenus(); + + if (menuData && menuData.length > 0) { + addDynamicRoutes(menuData); + dynamicRoutesAdded = true; + routesLoadingPromise = null; + return Promise.resolve(); + } else { + // 即使权限菜单为空,也要注册内置文章中心路由。 + addDynamicRoutes([]); + dynamicRoutesAdded = true; + routesLoadingPromise = null; + return Promise.resolve(); + } + } catch (error) { + console.error('加载动态路由失败:', error); + dynamicRoutesAdded = true; + routesLoadingPromise = null; + throw error; + } + })(); + + return routesLoadingPromise; +} + +// 核心修改:移除扁平化,直接使用嵌套菜单生成路由 +function addDynamicRoutes(menus) { + if (!menus?.length) { + return; + } + + // 直接转换嵌套菜单为嵌套路由(不再扁平化) + const dynamicRoutes = convertMenusToRoutes(menus); + + if (router.hasRoute('Main')) { + router.removeRoute('Main'); + } + + // 重新添加主路由,合并静态子路由和动态路由 + router.addRoute({ + path: "/", + name: "Main", + component: () => import("@/views/Main.vue"), + meta: { requiresAuth: true }, + children: [...staticMainChildren, ...dynamicRoutes] // 合并静态和动态子路由 + }); + + dynamicRoutesAdded = true; +} + +function findRouteByName(routes, routeName) { + for (const route of routes) { + if (route.name === routeName) { + return route; + } + if (route.children) { + const found = findRouteByName(route.children, routeName); + if (found) { + return found; + } + } + } + return null; +} + +function findFirstValidRoute(routes) { + for (const route of routes) { + if (route.component) { + return route; + } + if (route.children && route.children.length > 0) { + const childRoute = findFirstValidRoute(route.children); + if (childRoute) { + return childRoute; + } + } + } + return null; +} + +router.beforeEach(async (to, from, next) => { + const token = localStorage.getItem("token"); + const publicPaths = ["/login", "/register", "/forget"]; + + if (publicPaths.includes(to.path)) { + if (token) { + if (!dynamicRoutesAdded) { + await loadAndAddDynamicRoutes(); + } + next({ path: "/home" }); + } else { + next(); + } + return; + } + + if (!token) { + next({ path: "/login", query: { redirect: to.path } }); + return; + } + + if (!dynamicRoutesAdded) { + await loadAndAddDynamicRoutes(); + // 路由加载后重新导航,确保路由匹配正确 + next({ path: to.path, replace: true }); + return; + } + + next(); +}); + +export default router; diff --git a/backend/src/views/apps/erp/ProgramManagement/index.vue b/backend/src/views/apps/erp/ProgramManagement/index.vue new file mode 100644 index 0000000..8577758 --- /dev/null +++ b/backend/src/views/apps/erp/ProgramManagement/index.vue @@ -0,0 +1,209 @@ + + + + + 基础设置 + 配置ERP系统基础参数,包括公司信息、税务、单据、库存、财务等 + + + 恢复默认 + 保存设置 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/views/apps/erp/accountSet/components/edit.vue b/backend/src/views/apps/erp/accountSet/components/edit.vue new file mode 100644 index 0000000..224b0ec --- /dev/null +++ b/backend/src/views/apps/erp/accountSet/components/edit.vue @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 保存 + + + + + diff --git a/backend/src/views/apps/erp/accountSet/index.vue b/backend/src/views/apps/erp/accountSet/index.vue new file mode 100644 index 0000000..7cf6efa --- /dev/null +++ b/backend/src/views/apps/erp/accountSet/index.vue @@ -0,0 +1,253 @@ + + + + + 账套管理 + 管理企业账套信息,支持多账套切换与独立核算 + + + 新增账套 + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + {{ row.set_name }} + + + + + + + {{ row.start_year }}-{{ String(row.start_month).padStart(2, '0') }} + + + + + + {{ row.accounting_standard === '1' ? '企业会计准则' : '小企业会计准则' }} + + + + + + + + + {{ row.status === '1' ? '启用' : '禁用' }} + + + + + + 编辑 + + {{ row.status === '1' ? '禁用' : '启用' }} + + 删除 + + + + + + + + + + + + + + + + diff --git a/backend/src/views/apps/erp/accountSets/components/edit.vue b/backend/src/views/apps/erp/accountSets/components/edit.vue new file mode 100644 index 0000000..224b0ec --- /dev/null +++ b/backend/src/views/apps/erp/accountSets/components/edit.vue @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 保存 + + + + + diff --git a/backend/src/views/apps/erp/accountSets/index.vue b/backend/src/views/apps/erp/accountSets/index.vue new file mode 100644 index 0000000..7cf6efa --- /dev/null +++ b/backend/src/views/apps/erp/accountSets/index.vue @@ -0,0 +1,253 @@ + + + + + 账套管理 + 管理企业账套信息,支持多账套切换与独立核算 + + + 新增账套 + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + {{ row.set_name }} + + + + + + + {{ row.start_year }}-{{ String(row.start_month).padStart(2, '0') }} + + + + + + {{ row.accounting_standard === '1' ? '企业会计准则' : '小企业会计准则' }} + + + + + + + + + {{ row.status === '1' ? '启用' : '禁用' }} + + + + + + 编辑 + + {{ row.status === '1' ? '禁用' : '启用' }} + + 删除 + + + + + + + + + + + + + + + + diff --git a/backend/src/views/apps/erp/components/contactBook.vue b/backend/src/views/apps/erp/components/contactBook.vue new file mode 100644 index 0000000..ed7ce81 --- /dev/null +++ b/backend/src/views/apps/erp/components/contactBook.vue @@ -0,0 +1,453 @@ + + emit('update:visible', val)" + :title="companyName + ' - 通讯录'" + direction="rtl" + size="850px" + @closed="handleClosed" + > + + + 新增联系人 + 共 {{ list.length }} 位联系人 + + + + + + + {{ row.name }} + 主要 + + + + {{ genderText(row.gender) }} + + + + + {{ row.status === 1 ? '在职' : '离职' }} + + + + + + + + {{ formatMobiles(row.mobiles) }} + + + + + 编辑 + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 添加手机号({{ form.mobiles.length }}/5) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 保存 + + + + + + + + {{ detailData.name }} + {{ genderText(detailData.gender) }} + {{ detailData.department || '-' }} + {{ detailData.position || '-' }} + {{ detailData.phone || '-' }} + {{ detailData.email || '-' }} + {{ detailData.wechat || '-' }} + {{ detailData.qq || '-' }} + {{ detailData.dingtalk || '-' }} + + 是 + 否 + + + + {{ detailData.status === 1 ? '在职' : '离职' }} + + + {{ formatMobiles(detailData.mobiles) }} + {{ detailData.home_address || '-' }} + {{ detailData.remark || '-' }} + + + + 关闭 + 编辑 + + + + + + + + diff --git a/backend/src/views/apps/erp/customer/components/edit.vue b/backend/src/views/apps/erp/customer/components/edit.vue new file mode 100644 index 0000000..c051d1a --- /dev/null +++ b/backend/src/views/apps/erp/customer/components/edit.vue @@ -0,0 +1,452 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 万元 + + + + + + + 万元 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 发票信息 + + + + + + + + + + + + + + + + + + + + + + + 银行信息 + + + + + + + + + + + + + + + + + + + + + + 确认所有信息无误后,点击「保存」完成创建。 + + + + + + + + + + + + + diff --git a/backend/src/views/apps/erp/customer/index.vue b/backend/src/views/apps/erp/customer/index.vue new file mode 100644 index 0000000..34e42d3 --- /dev/null +++ b/backend/src/views/apps/erp/customer/index.vue @@ -0,0 +1,473 @@ + + + + + 客户管理 + 管理企业客户信息,支持多维度筛选与全生命周期追踪 + + + 新增客户 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + {{ row.customer_name }} + 草稿 + + + + + + + {{ customerTypeText(row.customer_type) }} + + + + + + + {{ customerLevelText(row.customer_level) }} + + + + + + + + + + + {{ statusText(row.status) }} + + + + + + {{ formatDateTime(row.update_time) }} + + + + + 通讯录 + 编辑 + + {{ row.status === '1' ? '禁用' : '启用' }} + + 删除 + + + + + + + + + + + + + + + + {{ detailData.customer_name }} + {{ customerTypeText(detailData.customer_type) }} + {{ customerLevelText(detailData.customer_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/apps/erp/customers/components/edit.vue b/backend/src/views/apps/erp/customers/components/edit.vue new file mode 100644 index 0000000..c051d1a --- /dev/null +++ b/backend/src/views/apps/erp/customers/components/edit.vue @@ -0,0 +1,452 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 万元 + + + + + + + 万元 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 发票信息 + + + + + + + + + + + + + + + + + + + + + + + 银行信息 + + + + + + + + + + + + + + + + + + + + + + 确认所有信息无误后,点击「保存」完成创建。 + + + + + + + + + + + + + diff --git a/backend/src/views/apps/erp/customers/index.vue b/backend/src/views/apps/erp/customers/index.vue new file mode 100644 index 0000000..34e42d3 --- /dev/null +++ b/backend/src/views/apps/erp/customers/index.vue @@ -0,0 +1,473 @@ + + + + + 客户管理 + 管理企业客户信息,支持多维度筛选与全生命周期追踪 + + + 新增客户 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + {{ row.customer_name }} + 草稿 + + + + + + + {{ customerTypeText(row.customer_type) }} + + + + + + + {{ customerLevelText(row.customer_level) }} + + + + + + + + + + + {{ statusText(row.status) }} + + + + + + {{ formatDateTime(row.update_time) }} + + + + + 通讯录 + 编辑 + + {{ row.status === '1' ? '禁用' : '启用' }} + + 删除 + + + + + + + + + + + + + + + + {{ detailData.customer_name }} + {{ customerTypeText(detailData.customer_type) }} + {{ customerLevelText(detailData.customer_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/apps/erp/supplier/components/edit.vue b/backend/src/views/apps/erp/supplier/components/edit.vue new file mode 100644 index 0000000..884ecd0 --- /dev/null +++ b/backend/src/views/apps/erp/supplier/components/edit.vue @@ -0,0 +1,452 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 万元 + + + + + + + 万元 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 发票信息 + + + + + + + + + + + + + + + + + + + + + + + 银行信息 + + + + + + + + + + + + + + + + + + + + + + 确认所有信息无误后,点击「保存」完成创建。 + + + + + + + + + + + + + diff --git a/backend/src/views/apps/erp/supplier/index.vue b/backend/src/views/apps/erp/supplier/index.vue new file mode 100644 index 0000000..c2631c8 --- /dev/null +++ b/backend/src/views/apps/erp/supplier/index.vue @@ -0,0 +1,473 @@ + + + + + 供应商管理 + 管理企业供应商信息,支持多维度筛选与全生命周期追踪 + + + 新增供应商 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + {{ row.supplier_name }} + 草稿 + + + + + + + {{ supplierTypeText(row.supplier_type) }} + + + + + + + {{ supplierLevelText(row.supplier_level) }} + + + + + + + + + + + {{ statusText(row.status) }} + + + + + + {{ formatDateTime(row.update_time) }} + + + + + 通讯录 + 编辑 + + {{ row.status === '1' ? '禁用' : '启用' }} + + 删除 + + + + + + + + + + + + + + + + {{ 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/apps/erp/suppliers/components/edit.vue b/backend/src/views/apps/erp/suppliers/components/edit.vue new file mode 100644 index 0000000..884ecd0 --- /dev/null +++ b/backend/src/views/apps/erp/suppliers/components/edit.vue @@ -0,0 +1,452 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 万元 + + + + + + + 万元 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 发票信息 + + + + + + + + + + + + + + + + + + + + + + + 银行信息 + + + + + + + + + + + + + + + + + + + + + + 确认所有信息无误后,点击「保存」完成创建。 + + + + + + + + + + + + + diff --git a/backend/src/views/apps/erp/suppliers/index.vue b/backend/src/views/apps/erp/suppliers/index.vue new file mode 100644 index 0000000..c2631c8 --- /dev/null +++ b/backend/src/views/apps/erp/suppliers/index.vue @@ -0,0 +1,473 @@ + + + + + 供应商管理 + 管理企业供应商信息,支持多维度筛选与全生命周期追踪 + + + 新增供应商 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + + + {{ row.supplier_name }} + 草稿 + + + + + + + {{ supplierTypeText(row.supplier_type) }} + + + + + + + {{ supplierLevelText(row.supplier_level) }} + + + + + + + + + + + {{ statusText(row.status) }} + + + + + + {{ formatDateTime(row.update_time) }} + + + + + 通讯录 + 编辑 + + {{ row.status === '1' ? '禁用' : '启用' }} + + 删除 + + + + + + + + + + + + + + + + {{ 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/user/notifyConfig.vue b/backend/src/views/user/notifyConfig.vue new file mode 100644 index 0000000..4347792 --- /dev/null +++ b/backend/src/views/user/notifyConfig.vue @@ -0,0 +1,232 @@ + + + + 通知设置 + + + + + + + + + 日程提醒将发送到此邮箱,支持重复提醒与确认回执 + + + + + 仅发送一次提醒,不支持重复提醒 + + + + + Bark 是 iOS 推送工具,App Store 下载后打开即可看到设备 Key + + + + + 使用官方 Bark 服务可留空 + + + + 保存设置 + 发送测试提醒 + 重置 + + + + + + + + 选择要测试的提醒渠道,将立即发送一条测试提醒到对应配置。 + + 邮箱 + 短信 + Bark + 站内信 + + + + 取消 + 发送 + + + + + + + + diff --git a/contact_screenshot.png b/contact_screenshot.png new file mode 100644 index 0000000..e37f554 Binary files /dev/null and b/contact_screenshot.png differ diff --git a/go/controllers/backend_erp_account_set.go b/go/controllers/backend_erp_account_set.go new file mode 100644 index 0000000..d6105f9 --- /dev/null +++ b/go/controllers/backend_erp_account_set.go @@ -0,0 +1,338 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + beego "github.com/beego/beego/v2/server/web" +) + +// BackendErpAccountSetController ERP账套管理控制器 +type BackendErpAccountSetController struct { + beego.Controller +} + +func (c *BackendErpAccountSetController) asClaims() (*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 *BackendErpAccountSetController) asJsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *BackendErpAccountSetController) asOk(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +type accountSetFormPayload struct { + SetCode string `json:"set_code"` + SetName string `json:"set_name"` + CompanyName string `json:"company_name"` + CompanyTaxNo string `json:"company_tax_no"` + StartYear int `json:"start_year"` + StartMonth int `json:"start_month"` + AccountingStandard string `json:"accounting_standard"` + Currency string `json:"currency"` + TaxRate float64 `json:"tax_rate"` + Status string `json:"status"` + Remark string `json:"remark"` +} + +// List GET /backend/erp/account-set/list +func (c *BackendErpAccountSetController) List() { + claims, err := c.asClaims() + if err != nil { + c.asJsonErr(401, 401, err.Error()) + return + } + + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + keyword := strings.TrimSpace(c.GetString("keyword")) + status := strings.TrimSpace(c.GetString("status")) + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + qs := models.Orm.QueryTable(new(models.ErpAccountSet)). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Filter("delete_time__isnull", true) + + if keyword != "" { + qs = qs.Filter("set_name__contains", keyword) + } + if status != "" { + qs = qs.Filter("status", status) + } + + total, _ := qs.Count() + + var list []models.ErpAccountSet + _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&list) + if err != nil { + c.asJsonErr(500, 500, "查询失败: "+err.Error()) + return + } + + c.asOk(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }) +} + +// Detail GET /backend/erp/account-set/:id +func (c *BackendErpAccountSetController) Detail() { + claims, err := c.asClaims() + if err != nil { + c.asJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.asJsonErr(400, 400, "无效的ID") + return + } + + var set models.ErpAccountSet + err = models.Orm.QueryTable(new(models.ErpAccountSet)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + One(&set) + if err != nil { + c.asJsonErr(404, 404, "账套未找到") + return + } + + c.asOk(set) +} + +// Create POST /backend/erp/account-set +func (c *BackendErpAccountSetController) Create() { + claims, err := c.asClaims() + if err != nil { + c.asJsonErr(401, 401, err.Error()) + return + } + + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.asJsonErr(400, 400, "参数错误") + return + } + var p accountSetFormPayload + if err := json.Unmarshal(raw, &p); err != nil { + c.asJsonErr(400, 400, "参数错误") + return + } + + if strings.TrimSpace(p.SetName) == "" { + c.asJsonErr(400, 400, "账套名称不能为空") + return + } + if strings.TrimSpace(p.SetCode) == "" { + c.asJsonErr(400, 400, "账套编码不能为空") + return + } + + set := models.ErpAccountSet{ + TenantID: fmt.Sprintf("%d", claims.TenantId), + SetCode: strings.TrimSpace(p.SetCode), + SetName: strings.TrimSpace(p.SetName), + CompanyName: strings.TrimSpace(p.CompanyName), + CompanyTaxNo: strings.TrimSpace(p.CompanyTaxNo), + StartYear: p.StartYear, + StartMonth: p.StartMonth, + AccountingStandard: p.AccountingStandard, + Currency: strings.TrimSpace(p.Currency), + TaxRate: p.TaxRate, + Status: p.Status, + Remark: p.Remark, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + + if set.StartYear == 0 { + set.StartYear = time.Now().Year() + } + if set.StartMonth == 0 { + set.StartMonth = 1 + } + if set.AccountingStandard == "" { + set.AccountingStandard = "1" + } + if set.Currency == "" { + set.Currency = "CNY" + } + if set.TaxRate == 0 { + set.TaxRate = 13.00 + } + if set.Status == "" { + set.Status = "1" + } + + id, err := models.Orm.Insert(&set) + if err != nil { + c.asJsonErr(500, 500, "创建失败: "+err.Error()) + return + } + + c.asOk(map[string]interface{}{"id": id}) +} + +// Update PUT /backend/erp/account-set/:id +func (c *BackendErpAccountSetController) Update() { + claims, err := c.asClaims() + if err != nil { + c.asJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.asJsonErr(400, 400, "无效的ID") + return + } + + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.asJsonErr(400, 400, "参数错误") + return + } + var p accountSetFormPayload + if err := json.Unmarshal(raw, &p); err != nil { + c.asJsonErr(400, 400, "参数错误") + return + } + + var set models.ErpAccountSet + err = models.Orm.QueryTable(new(models.ErpAccountSet)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + One(&set) + if err != nil { + c.asJsonErr(404, 404, "账套未找到") + return + } + + set.SetCode = strings.TrimSpace(p.SetCode) + set.SetName = strings.TrimSpace(p.SetName) + set.CompanyName = strings.TrimSpace(p.CompanyName) + set.CompanyTaxNo = strings.TrimSpace(p.CompanyTaxNo) + set.StartYear = p.StartYear + set.StartMonth = p.StartMonth + set.AccountingStandard = p.AccountingStandard + set.Currency = strings.TrimSpace(p.Currency) + set.TaxRate = p.TaxRate + set.Status = p.Status + set.Remark = p.Remark + set.UpdateTime = time.Now() + + _, err = models.Orm.Update(&set) + if err != nil { + c.asJsonErr(500, 500, "更新失败: "+err.Error()) + return + } + + c.asOk(nil) +} + +// Delete DELETE /backend/erp/account-set/:id +func (c *BackendErpAccountSetController) Delete() { + claims, err := c.asClaims() + if err != nil { + c.asJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.asJsonErr(400, 400, "无效的ID") + return + } + + now := time.Now() + _, err = models.Orm.QueryTable(new(models.ErpAccountSet)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Update(map[string]interface{}{ + "delete_time": now, + "update_time": now, + }) + if err != nil { + c.asJsonErr(500, 500, "删除失败: "+err.Error()) + return + } + + c.asOk(nil) +} + +// ToggleStatus POST /backend/erp/account-set/:id/status +func (c *BackendErpAccountSetController) ToggleStatus() { + claims, err := c.asClaims() + if err != nil { + c.asJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.asJsonErr(400, 400, "无效的ID") + return + } + + raw, _ := io.ReadAll(c.Ctx.Request.Body) + var p struct { + Status string `json:"status"` + } + _ = json.Unmarshal(raw, &p) + + _, err = models.Orm.QueryTable(new(models.ErpAccountSet)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Update(map[string]interface{}{ + "status": p.Status, + "update_time": time.Now(), + }) + if err != nil { + c.asJsonErr(500, 500, "操作失败: "+err.Error()) + return + } + + c.asOk(nil) +} diff --git a/go/controllers/backend_erp_company_contact.go b/go/controllers/backend_erp_company_contact.go new file mode 100644 index 0000000..5460c42 --- /dev/null +++ b/go/controllers/backend_erp_company_contact.go @@ -0,0 +1,263 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + beego "github.com/beego/beego/v2/server/web" +) + +// BackendErpCompanyContactController ERP客户/供应商联系人控制器 +type BackendErpCompanyContactController struct { + beego.Controller +} + +func (c *BackendErpCompanyContactController) contactClaims() (*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 *BackendErpCompanyContactController) contactJsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *BackendErpCompanyContactController) contactOk(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +type companyContactPayload struct { + CompanyType string `json:"company_type"` + CompanyID uint64 `json:"company_id"` + Name string `json:"name"` + Gender int8 `json:"gender"` + Phone string `json:"phone"` + Mobiles string `json:"mobiles"` // JSON数组字符串 + Wechat string `json:"wechat"` + QQ string `json:"qq"` + Dingtalk string `json:"dingtalk"` + Email string `json:"email"` + Department string `json:"department"` + Position string `json:"position"` + HomeAddress string `json:"home_address"` + IsPrimary int8 `json:"is_primary"` + Status int8 `json:"status"` + Remark string `json:"remark"` +} + +// List GET /backend/erp/company-contact/list?company_type=customer&company_id=1 +func (c *BackendErpCompanyContactController) List() { + claims, err := c.contactClaims() + if err != nil { + c.contactJsonErr(401, 401, err.Error()) + return + } + + companyType := strings.TrimSpace(c.GetString("company_type")) + companyIDStr := strings.TrimSpace(c.GetString("company_id")) + + if companyType == "" || companyIDStr == "" { + c.contactJsonErr(400, 400, "缺少公司参数") + return + } + + companyID, err := strconv.ParseUint(companyIDStr, 10, 64) + if err != nil { + c.contactJsonErr(400, 400, "公司ID格式错误") + return + } + + qs := models.Orm.QueryTable(new(models.ErpCompanyContact)). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Filter("company_type", companyType). + Filter("company_id", companyID). + Filter("delete_time__isnull", true) + + var list []models.ErpCompanyContact + _, err = qs.OrderBy("-is_primary", "id").All(&list) + if err != nil { + c.contactJsonErr(500, 500, "查询失败: "+err.Error()) + return + } + + c.contactOk(map[string]interface{}{"list": list}) +} + +// Create POST /backend/erp/company-contact +func (c *BackendErpCompanyContactController) Create() { + claims, err := c.contactClaims() + if err != nil { + c.contactJsonErr(401, 401, err.Error()) + return + } + + body, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.contactJsonErr(400, 400, "读取请求体失败") + return + } + var p companyContactPayload + if err := json.Unmarshal(body, &p); err != nil { + c.contactJsonErr(400, 400, "参数格式错误") + return + } + + if strings.TrimSpace(p.Name) == "" { + c.contactJsonErr(400, 400, "联系人姓名不能为空") + return + } + if p.CompanyType == "" || p.CompanyID == 0 { + c.contactJsonErr(400, 400, "缺少公司参数") + return + } + + contact := models.ErpCompanyContact{ + TenantID: fmt.Sprintf("%d", claims.TenantId), + CompanyType: p.CompanyType, + CompanyID: p.CompanyID, + Name: strings.TrimSpace(p.Name), + Gender: p.Gender, + Phone: strings.TrimSpace(p.Phone), + Mobiles: p.Mobiles, + Wechat: strings.TrimSpace(p.Wechat), + QQ: strings.TrimSpace(p.QQ), + Dingtalk: strings.TrimSpace(p.Dingtalk), + Email: strings.TrimSpace(p.Email), + Department: strings.TrimSpace(p.Department), + Position: strings.TrimSpace(p.Position), + HomeAddress: strings.TrimSpace(p.HomeAddress), + IsPrimary: p.IsPrimary, + Status: p.Status, + Remark: strings.TrimSpace(p.Remark), + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + + id, err := models.Orm.Insert(&contact) + if err != nil { + c.contactJsonErr(500, 500, "创建失败: "+err.Error()) + return + } + + c.contactOk(map[string]interface{}{"id": id}) +} + +// Update PUT /backend/erp/company-contact/:id +func (c *BackendErpCompanyContactController) Update() { + claims, err := c.contactClaims() + if err != nil { + c.contactJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + c.contactJsonErr(400, 400, "ID格式错误") + return + } + + body, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.contactJsonErr(400, 400, "读取请求体失败") + return + } + var p companyContactPayload + if err := json.Unmarshal(body, &p); err != nil { + c.contactJsonErr(400, 400, "参数格式错误") + return + } + + contact := models.ErpCompanyContact{ID: id} + if err := models.Orm.Read(&contact); err != nil { + c.contactJsonErr(404, 404, "联系人不存在") + return + } + if contact.TenantID != fmt.Sprintf("%d", claims.TenantId) { + c.contactJsonErr(403, 403, "无权操作") + return + } + + contact.Name = strings.TrimSpace(p.Name) + contact.Gender = p.Gender + contact.Phone = strings.TrimSpace(p.Phone) + contact.Mobiles = p.Mobiles + contact.Wechat = strings.TrimSpace(p.Wechat) + contact.QQ = strings.TrimSpace(p.QQ) + contact.Dingtalk = strings.TrimSpace(p.Dingtalk) + contact.Email = strings.TrimSpace(p.Email) + contact.Department = strings.TrimSpace(p.Department) + contact.Position = strings.TrimSpace(p.Position) + contact.HomeAddress = strings.TrimSpace(p.HomeAddress) + contact.IsPrimary = p.IsPrimary + contact.Status = p.Status + contact.Remark = strings.TrimSpace(p.Remark) + contact.UpdateTime = time.Now() + + _, err = models.Orm.Update(&contact) + if err != nil { + c.contactJsonErr(500, 500, "更新失败: "+err.Error()) + return + } + + c.contactOk(nil) +} + +// Delete DELETE /backend/erp/company-contact/:id +func (c *BackendErpCompanyContactController) Delete() { + claims, err := c.contactClaims() + if err != nil { + c.contactJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + c.contactJsonErr(400, 400, "ID格式错误") + return + } + + contact := models.ErpCompanyContact{ID: id} + if err := models.Orm.Read(&contact); err != nil { + c.contactJsonErr(404, 404, "联系人不存在") + return + } + if contact.TenantID != fmt.Sprintf("%d", claims.TenantId) { + c.contactJsonErr(403, 403, "无权操作") + return + } + + now := time.Now() + contact.DeleteTime = &now + _, err = models.Orm.Update(&contact, "delete_time") + if err != nil { + c.contactJsonErr(500, 500, "删除失败: "+err.Error()) + return + } + + c.contactOk(nil) +} diff --git a/go/controllers/backend_erp_customer.go b/go/controllers/backend_erp_customer.go new file mode 100644 index 0000000..af3178a --- /dev/null +++ b/go/controllers/backend_erp_customer.go @@ -0,0 +1,419 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + beego "github.com/beego/beego/v2/server/web" +) + +// BackendErpCustomerController ERP客户管理控制器 +type BackendErpCustomerController struct { + beego.Controller +} + +func (c *BackendErpCustomerController) customerClaims() (*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 *BackendErpCustomerController) customerJsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *BackendErpCustomerController) customerOk(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +type customerFormPayload struct { + CustomerName string `json:"customer_name"` + CustomerType string `json:"customer_type"` + ContactPerson string `json:"contact_person"` + ContactPhone string `json:"contact_phone"` + ContactEmail string `json:"contact_email"` + CustomerLevel string `json:"customer_level"` + Industry string `json:"industry"` + RegisteredCapital string `json:"registered_capital"` + PaidCapital string `json:"paid_capital"` + EstablishDate string `json:"establish_date"` + AdministrativeDivision string `json:"administrative_division"` + EnterpriseType string `json:"enterprise_type"` + TaxpayerQualification string `json:"taxpayer_qualification"` + BusinessScope string `json:"business_scope"` + Address string `json:"address"` + RegisterTime string `json:"register_time"` + ExpireTime string `json:"expire_time"` + Status string `json:"status"` + IsDraft int8 `json:"is_draft"` + Remark string `json:"remark"` + InvoiceTitle string `json:"invoice_title"` + TaxNumber string `json:"tax_number"` + BankName string `json:"bank_name"` + BankAccount string `json:"bank_account"` + RegisteredAddress string `json:"registered_address"` + RegisteredPhone string `json:"registered_phone"` +} + +// List GET /backend/erp/customer/list +func (c *BackendErpCustomerController) List() { + claims, err := c.customerClaims() + if err != nil { + c.customerJsonErr(401, 401, err.Error()) + return + } + + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + keyword := strings.TrimSpace(c.GetString("keyword")) + customerType := strings.TrimSpace(c.GetString("customer_type")) + customerLevel := strings.TrimSpace(c.GetString("customer_level")) + status := strings.TrimSpace(c.GetString("status")) + isDraft := strings.TrimSpace(c.GetString("is_draft")) + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + qs := models.Orm.QueryTable(new(models.TenantCrmCustomer)). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Filter("delete_time__isnull", true) + + if keyword != "" { + qs = qs.Filter("customer_name__contains", keyword) + } + if customerType != "" { + qs = qs.Filter("customer_type", customerType) + } + if customerLevel != "" { + qs = qs.Filter("customer_level", customerLevel) + } + if status != "" { + qs = qs.Filter("status", status) + } + if isDraft != "" { + if isDraft == "1" { + qs = qs.Filter("is_draft", 1) + } else { + qs = qs.Filter("is_draft", 0) + } + } + + total, _ := qs.Count() + + var list []models.TenantCrmCustomer + _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&list) + if err != nil { + c.customerJsonErr(500, 500, "查询失败: "+err.Error()) + return + } + + c.customerOk(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }) +} + +// Detail GET /backend/erp/customer/:id +func (c *BackendErpCustomerController) Detail() { + claims, err := c.customerClaims() + if err != nil { + c.customerJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.customerJsonErr(400, 400, "无效的ID") + return + } + + var customer models.TenantCrmCustomer + err = models.Orm.QueryTable(new(models.TenantCrmCustomer)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + One(&customer) + if err != nil { + c.customerJsonErr(404, 404, "客户未找到") + return + } + + c.customerOk(customer) +} + +// Create POST /backend/erp/customer +func (c *BackendErpCustomerController) Create() { + claims, err := c.customerClaims() + if err != nil { + c.customerJsonErr(401, 401, err.Error()) + return + } + + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.customerJsonErr(400, 400, "参数错误") + return + } + var p customerFormPayload + if err := json.Unmarshal(raw, &p); err != nil { + c.customerJsonErr(400, 400, "参数错误") + return + } + + if strings.TrimSpace(p.CustomerName) == "" { + c.customerJsonErr(400, 400, "客户名称不能为空") + return + } + // 正式保存时验证联系电话,草稿不强制 + if p.IsDraft == 0 && strings.TrimSpace(p.ContactPhone) == "" { + c.customerJsonErr(400, 400, "联系人电话不能为空") + return + } + + customer := models.TenantCrmCustomer{ + TenantID: fmt.Sprintf("%d", claims.TenantId), + CustomerName: strings.TrimSpace(p.CustomerName), + CustomerType: p.CustomerType, + ContactPerson: strings.TrimSpace(p.ContactPerson), + ContactPhone: strings.TrimSpace(p.ContactPhone), + ContactEmail: strings.TrimSpace(p.ContactEmail), + CustomerLevel: p.CustomerLevel, + Industry: strings.TrimSpace(p.Industry), + RegisteredCapital: strings.TrimSpace(p.RegisteredCapital), + PaidCapital: strings.TrimSpace(p.PaidCapital), + AdministrativeDivision: strings.TrimSpace(p.AdministrativeDivision), + EnterpriseType: strings.TrimSpace(p.EnterpriseType), + TaxpayerQualification: strings.TrimSpace(p.TaxpayerQualification), + BusinessScope: strings.TrimSpace(p.BusinessScope), + Address: strings.TrimSpace(p.Address), + Status: p.Status, + IsDraft: p.IsDraft, + Remark: p.Remark, + InvoiceTitle: strings.TrimSpace(p.InvoiceTitle), + TaxNumber: strings.TrimSpace(p.TaxNumber), + BankName: strings.TrimSpace(p.BankName), + BankAccount: strings.TrimSpace(p.BankAccount), + RegisteredAddress: strings.TrimSpace(p.RegisteredAddress), + RegisteredPhone: strings.TrimSpace(p.RegisteredPhone), + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + + if p.RegisterTime != "" { + if t, err := time.ParseInLocation("2006-01-02", p.RegisterTime, time.Local); err == nil { + customer.RegisterTime = &t + } + } + if p.ExpireTime != "" { + if t, err := time.ParseInLocation("2006-01-02", p.ExpireTime, time.Local); err == nil { + customer.ExpireTime = &t + } + } + if p.EstablishDate != "" { + if t, err := time.ParseInLocation("2006-01-02", p.EstablishDate, time.Local); err == nil { + customer.EstablishDate = &t + } + } + if customer.CustomerLevel == "" { + customer.CustomerLevel = "3" + } + if customer.Status == "" { + customer.Status = "1" + } + + id, err := models.Orm.Insert(&customer) + if err != nil { + c.customerJsonErr(500, 500, "创建失败: "+err.Error()) + return + } + + c.customerOk(map[string]interface{}{"id": id}) +} + +// Update PUT /backend/erp/customer/:id +func (c *BackendErpCustomerController) Update() { + claims, err := c.customerClaims() + if err != nil { + c.customerJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.customerJsonErr(400, 400, "无效的ID") + return + } + + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.customerJsonErr(400, 400, "参数错误") + return + } + var p customerFormPayload + if err := json.Unmarshal(raw, &p); err != nil { + c.customerJsonErr(400, 400, "参数错误") + return + } + + var customer models.TenantCrmCustomer + err = models.Orm.QueryTable(new(models.TenantCrmCustomer)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + One(&customer) + if err != nil { + c.customerJsonErr(404, 404, "客户未找到") + return + } + + customer.CustomerName = strings.TrimSpace(p.CustomerName) + customer.CustomerType = p.CustomerType + customer.ContactPerson = strings.TrimSpace(p.ContactPerson) + customer.ContactPhone = strings.TrimSpace(p.ContactPhone) + customer.ContactEmail = strings.TrimSpace(p.ContactEmail) + customer.CustomerLevel = p.CustomerLevel + customer.Industry = strings.TrimSpace(p.Industry) + customer.RegisteredCapital = strings.TrimSpace(p.RegisteredCapital) + customer.PaidCapital = strings.TrimSpace(p.PaidCapital) + customer.AdministrativeDivision = strings.TrimSpace(p.AdministrativeDivision) + customer.EnterpriseType = strings.TrimSpace(p.EnterpriseType) + customer.TaxpayerQualification = strings.TrimSpace(p.TaxpayerQualification) + customer.BusinessScope = strings.TrimSpace(p.BusinessScope) + customer.Address = strings.TrimSpace(p.Address) + customer.Status = p.Status + customer.IsDraft = p.IsDraft + customer.Remark = p.Remark + customer.InvoiceTitle = strings.TrimSpace(p.InvoiceTitle) + customer.TaxNumber = strings.TrimSpace(p.TaxNumber) + customer.BankName = strings.TrimSpace(p.BankName) + customer.BankAccount = strings.TrimSpace(p.BankAccount) + customer.RegisteredAddress = strings.TrimSpace(p.RegisteredAddress) + customer.RegisteredPhone = strings.TrimSpace(p.RegisteredPhone) + customer.UpdateTime = time.Now() + + if p.RegisterTime != "" { + if t, err := time.ParseInLocation("2006-01-02", p.RegisterTime, time.Local); err == nil { + customer.RegisterTime = &t + } + } else { + customer.RegisterTime = nil + } + if p.ExpireTime != "" { + if t, err := time.ParseInLocation("2006-01-02", p.ExpireTime, time.Local); err == nil { + customer.ExpireTime = &t + } + } else { + customer.ExpireTime = nil + } + if p.EstablishDate != "" { + if t, err := time.ParseInLocation("2006-01-02", p.EstablishDate, time.Local); err == nil { + customer.EstablishDate = &t + } + } else { + customer.EstablishDate = nil + } + + _, err = models.Orm.Update(&customer) + if err != nil { + c.customerJsonErr(500, 500, "更新失败: "+err.Error()) + return + } + + c.customerOk(nil) +} + +// Delete DELETE /backend/erp/customer/:id +func (c *BackendErpCustomerController) Delete() { + claims, err := c.customerClaims() + if err != nil { + c.customerJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.customerJsonErr(400, 400, "无效的ID") + return + } + + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmCustomer)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Update(map[string]interface{}{ + "delete_time": now, + "update_time": now, + }) + if err != nil { + c.customerJsonErr(500, 500, "删除失败: "+err.Error()) + return + } + + c.customerOk(nil) +} + +// ToggleStatus POST /backend/erp/customer/:id/status +func (c *BackendErpCustomerController) ToggleStatus() { + claims, err := c.customerClaims() + if err != nil { + c.customerJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.customerJsonErr(400, 400, "无效的ID") + return + } + + raw, _ := io.ReadAll(c.Ctx.Request.Body) + var p struct { + Status string `json:"status"` + } + _ = json.Unmarshal(raw, &p) + + _, err = models.Orm.QueryTable(new(models.TenantCrmCustomer)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Update(map[string]interface{}{ + "status": p.Status, + "update_time": time.Now(), + }) + if err != nil { + c.customerJsonErr(500, 500, "操作失败: "+err.Error()) + return + } + + c.customerOk(nil) +} diff --git a/go/controllers/backend_erp_normal_setting.go b/go/controllers/backend_erp_normal_setting.go new file mode 100644 index 0000000..b0ff56f --- /dev/null +++ b/go/controllers/backend_erp_normal_setting.go @@ -0,0 +1,288 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + beego "github.com/beego/beego/v2/server/web" +) + +// BackendErpNormalSettingController ERP基础设置控制器 +type BackendErpNormalSettingController struct { + beego.Controller +} + +func (c *BackendErpNormalSettingController) nsClaims() (*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 *BackendErpNormalSettingController) nsJsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *BackendErpNormalSettingController) nsOk(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +// 默认基础配置项定义 +var defaultErpNormalSettings = []struct { + Key string + Name string + Group string + Type string + Value string + Options string + SortOrder int +}{ + // 公司信息 + {"company_name", "公司名称", "company", "input", "", "", 1}, + {"company_address", "公司地址", "company", "input", "", "", 2}, + {"company_phone", "公司电话", "company", "input", "", "", 3}, + {"company_email", "公司邮箱", "company", "input", "", "", 4}, + {"company_fax", "公司传真", "company", "input", "", "", 5}, + {"company_website", "公司网址", "company", "input", "", "", 6}, + // 税务设置 + {"taxpayer_name", "纳税人名称", "tax", "input", "", "", 1}, + {"taxpayer_no", "纳税人识别号", "tax", "input", "", "", 2}, + {"default_tax_rate", "默认税率(%)", "tax", "number", "13", "", 3}, + {"tax_type", "纳税人类型", "tax", "select", "general", "[{\"label\":\"一般纳税人\",\"value\":\"general\"},{\"label\":\"小规模纳税人\",\"value\":\"small\"}]", 4}, + // 单据设置 + {"sale_order_prefix", "销售单号前缀", "document", "input", "XS", "", 1}, + {"purchase_order_prefix", "采购单号前缀", "document", "input", "CG", "", 2}, + {"stock_in_prefix", "入库单号前缀", "document", "input", "RK", "", 3}, + {"stock_out_prefix", "出库单号前缀", "document", "input", "CK", "", 4}, + {"return_order_prefix", "退货单号前缀", "document", "input", "TH", "", 5}, + {"auto_generate_doc_no", "自动生成单据号", "document", "switch", "1", "", 6}, + // 库存设置 + {"stock_warning_threshold", "库存预警阈值", "inventory", "number", "10", "", 1}, + {"default_warehouse", "默认仓库", "inventory", "input", "", "", 2}, + {"negative_stock_allowed", "允许负库存", "inventory", "switch", "0", "", 3}, + {"stock_check_cycle", "盘点周期(天)", "inventory", "number", "30", "", 4}, + // 财务设置 + {"default_currency", "默认币种", "finance", "select", "CNY", "[{\"label\":\"人民币(CNY)\",\"value\":\"CNY\"},{\"label\":\"美元(USD)\",\"value\":\"USD\"},{\"label\":\"欧元(EUR)\",\"value\":\"EUR\"}]", 1}, + {"default_payment_method", "默认付款方式", "finance", "select", "bank_transfer", "[{\"label\":\"银行转账\",\"value\":\"bank_transfer\"},{\"label\":\"现金\",\"value\":\"cash\"},{\"label\":\"支票\",\"value\":\"check\"},{\"label\":\"在线支付\",\"value\":\"online\"}]", 2}, + {"accounting_standard", "会计制度", "finance", "select", "enterprise", "[{\"label\":\"企业会计准则\",\"value\":\"enterprise\"},{\"label\":\"小企业会计准则\",\"value\":\"small_enterprise\"}]", 3}, + {"fiscal_year_start", "会计年度起始月", "finance", "number", "1", "", 4}, +} + +// List GET /backend/erp/normal-settings +// 获取租户所有基础设置,按分组返回;如果租户还没有配置,自动初始化默认配置 +func (c *BackendErpNormalSettingController) List() { + claims, err := c.nsClaims() + if err != nil { + c.nsJsonErr(401, 401, err.Error()) + return + } + + tenantID := fmt.Sprintf("%d", claims.TenantId) + + // 查询租户现有配置 + list, err := models.GetErpNormalSettingsByTenant(tenantID) + if err != nil { + c.nsJsonErr(500, 500, "查询失败: "+err.Error()) + return + } + + // 如果没有配置,自动初始化默认配置 + if len(list) == 0 { + now := time.Now() + for _, item := range defaultErpNormalSettings { + setting := models.ErpNormalSetting{ + TenantID: tenantID, + ConfigKey: item.Key, + ConfigValue: item.Value, + ConfigName: item.Name, + ConfigGroup: item.Group, + ConfigType: item.Type, + ConfigOptions: item.Options, + SortOrder: item.SortOrder, + Status: 1, + CreateTime: now, + UpdateTime: now, + } + _, _ = models.Orm.Insert(&setting) + } + // 重新查询 + list, _ = models.GetErpNormalSettingsByTenant(tenantID) + } + + // 按分组组织 + groups := make(map[string][]models.ErpNormalSetting) + var groupOrder []string + for _, item := range list { + if _, ok := groups[item.ConfigGroup]; !ok { + groupOrder = append(groupOrder, item.ConfigGroup) + } + groups[item.ConfigGroup] = append(groups[item.ConfigGroup], item) + } + + // 按固定顺序排列分组 + groupNameMap := map[string]string{ + "company": "公司信息", + "tax": "税务设置", + "document": "单据设置", + "inventory": "库存设置", + "finance": "财务设置", + } + + type groupResult struct { + Group string `json:"group"` + Name string `json:"name"` + Items []models.ErpNormalSetting `json:"items"` + } + + var result []groupResult + desiredOrder := []string{"company", "tax", "document", "inventory", "finance"} + for _, g := range desiredOrder { + if items, ok := groups[g]; ok { + result = append(result, groupResult{ + Group: g, + Name: groupNameMap[g], + Items: items, + }) + } + } + // 其他未在预设顺序中的分组 + for _, g := range groupOrder { + found := false + for _, d := range desiredOrder { + if g == d { + found = true + break + } + } + if !found { + result = append(result, groupResult{ + Group: g, + Name: groupNameMap[g], + Items: groups[g], + }) + } + } + + c.nsOk(result) +} + +// Save PUT /backend/erp/normal-settings +// 批量保存配置(接收 key-value 映射) +func (c *BackendErpNormalSettingController) Save() { + claims, err := c.nsClaims() + if err != nil { + c.nsJsonErr(401, 401, err.Error()) + return + } + + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.nsJsonErr(400, 400, "参数错误") + return + } + + // 支持两种格式:{"key1":"value1","key2":"value2"} 或 [{"config_key":"xxx","config_value":"xxx"}] + var kvMap map[string]string + if err := json.Unmarshal(raw, &kvMap); err != nil { + // 尝试数组格式 + var arr []struct { + ConfigKey string `json:"config_key"` + ConfigValue string `json:"config_value"` + } + if err2 := json.Unmarshal(raw, &arr); err2 != nil { + c.nsJsonErr(400, 400, "参数格式错误") + return + } + kvMap = make(map[string]string) + for _, item := range arr { + kvMap[item.ConfigKey] = item.ConfigValue + } + } + + tenantID := fmt.Sprintf("%d", claims.TenantId) + now := time.Now() + + for key, value := range kvMap { + setting := models.GetErpNormalSettingByKey(tenantID, key) + if setting != nil { + setting.ConfigValue = value + setting.UpdateTime = now + _, _ = models.Orm.Update(setting, "ConfigValue", "UpdateTime") + } else { + // 不存在则创建 + newSetting := models.ErpNormalSetting{ + TenantID: tenantID, + ConfigKey: key, + ConfigValue: value, + ConfigName: key, + ConfigGroup: "custom", + ConfigType: "input", + Status: 1, + CreateTime: now, + UpdateTime: now, + } + _, _ = models.Orm.Insert(&newSetting) + } + } + + c.nsOk(nil) +} + +// Reset POST /backend/erp/normal-settings/reset +// 重置为默认配置 +func (c *BackendErpNormalSettingController) Reset() { + claims, err := c.nsClaims() + if err != nil { + c.nsJsonErr(401, 401, err.Error()) + return + } + + tenantID := fmt.Sprintf("%d", claims.TenantId) + + // 删除现有配置 + _, _ = models.Orm.QueryTable(new(models.ErpNormalSetting)). + Filter("tenant_id", tenantID). + Delete() + + // 重新初始化默认配置 + now := time.Now() + for _, item := range defaultErpNormalSettings { + setting := models.ErpNormalSetting{ + TenantID: tenantID, + ConfigKey: item.Key, + ConfigValue: item.Value, + ConfigName: item.Name, + ConfigGroup: item.Group, + ConfigType: item.Type, + ConfigOptions: item.Options, + SortOrder: item.SortOrder, + Status: 1, + CreateTime: now, + UpdateTime: now, + } + _, _ = models.Orm.Insert(&setting) + } + + c.nsOk(nil) +} diff --git a/go/controllers/backend_erp_supplier.go b/go/controllers/backend_erp_supplier.go new file mode 100644 index 0000000..7e17337 --- /dev/null +++ b/go/controllers/backend_erp_supplier.go @@ -0,0 +1,419 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + beego "github.com/beego/beego/v2/server/web" +) + +// BackendErpSupplierController ERP供应商管理控制器 +type BackendErpSupplierController struct { + beego.Controller +} + +func (c *BackendErpSupplierController) supplierClaims() (*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 *BackendErpSupplierController) supplierJsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *BackendErpSupplierController) supplierOk(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +type supplierFormPayload struct { + SupplierName string `json:"supplier_name"` + SupplierType string `json:"supplier_type"` + ContactPerson string `json:"contact_person"` + ContactPhone string `json:"contact_phone"` + ContactEmail string `json:"contact_email"` + SupplierLevel string `json:"supplier_level"` + Industry string `json:"industry"` + RegisteredCapital string `json:"registered_capital"` + PaidCapital string `json:"paid_capital"` + EstablishDate string `json:"establish_date"` + AdministrativeDivision string `json:"administrative_division"` + EnterpriseType string `json:"enterprise_type"` + TaxpayerQualification string `json:"taxpayer_qualification"` + BusinessScope string `json:"business_scope"` + Address string `json:"address"` + RegisterTime string `json:"register_time"` + ExpireTime string `json:"expire_time"` + Status string `json:"status"` + IsDraft int8 `json:"is_draft"` + Remark string `json:"remark"` + InvoiceTitle string `json:"invoice_title"` + TaxNumber string `json:"tax_number"` + BankName string `json:"bank_name"` + BankAccount string `json:"bank_account"` + RegisteredAddress string `json:"registered_address"` + RegisteredPhone string `json:"registered_phone"` +} + +// List GET /backend/erp/supplier/list +func (c *BackendErpSupplierController) List() { + claims, err := c.supplierClaims() + if err != nil { + c.supplierJsonErr(401, 401, err.Error()) + return + } + + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + keyword := strings.TrimSpace(c.GetString("keyword")) + supplierType := strings.TrimSpace(c.GetString("supplier_type")) + supplierLevel := strings.TrimSpace(c.GetString("supplier_level")) + status := strings.TrimSpace(c.GetString("status")) + isDraft := strings.TrimSpace(c.GetString("is_draft")) + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + qs := models.Orm.QueryTable(new(models.TenantCrmSupplier)). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Filter("delete_time__isnull", true) + + if keyword != "" { + qs = qs.Filter("supplier_name__contains", keyword) + } + if supplierType != "" { + qs = qs.Filter("supplier_type", supplierType) + } + if supplierLevel != "" { + qs = qs.Filter("supplier_level", supplierLevel) + } + if status != "" { + qs = qs.Filter("status", status) + } + if isDraft != "" { + if isDraft == "1" { + qs = qs.Filter("is_draft", 1) + } else { + qs = qs.Filter("is_draft", 0) + } + } + + total, _ := qs.Count() + + var list []models.TenantCrmSupplier + _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&list) + if err != nil { + c.supplierJsonErr(500, 500, "查询失败: "+err.Error()) + return + } + + c.supplierOk(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }) +} + +// Detail GET /backend/erp/supplier/:id +func (c *BackendErpSupplierController) Detail() { + claims, err := c.supplierClaims() + if err != nil { + c.supplierJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.supplierJsonErr(400, 400, "无效的ID") + return + } + + var supplier models.TenantCrmSupplier + err = models.Orm.QueryTable(new(models.TenantCrmSupplier)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + One(&supplier) + if err != nil { + c.supplierJsonErr(404, 404, "供应商未找到") + return + } + + c.supplierOk(supplier) +} + +// Create POST /backend/erp/supplier +func (c *BackendErpSupplierController) Create() { + claims, err := c.supplierClaims() + if err != nil { + c.supplierJsonErr(401, 401, err.Error()) + return + } + + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.supplierJsonErr(400, 400, "参数错误") + return + } + var p supplierFormPayload + if err := json.Unmarshal(raw, &p); err != nil { + c.supplierJsonErr(400, 400, "参数错误") + return + } + + if strings.TrimSpace(p.SupplierName) == "" { + c.supplierJsonErr(400, 400, "供应商名称不能为空") + return + } + // 正式保存时验证联系电话,草稿不强制 + if p.IsDraft == 0 && strings.TrimSpace(p.ContactPhone) == "" { + c.supplierJsonErr(400, 400, "联系人电话不能为空") + return + } + + supplier := models.TenantCrmSupplier{ + TenantID: fmt.Sprintf("%d", claims.TenantId), + SupplierName: strings.TrimSpace(p.SupplierName), + SupplierType: p.SupplierType, + ContactPerson: strings.TrimSpace(p.ContactPerson), + ContactPhone: strings.TrimSpace(p.ContactPhone), + ContactEmail: strings.TrimSpace(p.ContactEmail), + SupplierLevel: p.SupplierLevel, + Industry: strings.TrimSpace(p.Industry), + RegisteredCapital: strings.TrimSpace(p.RegisteredCapital), + PaidCapital: strings.TrimSpace(p.PaidCapital), + AdministrativeDivision: strings.TrimSpace(p.AdministrativeDivision), + EnterpriseType: strings.TrimSpace(p.EnterpriseType), + TaxpayerQualification: strings.TrimSpace(p.TaxpayerQualification), + BusinessScope: strings.TrimSpace(p.BusinessScope), + Address: strings.TrimSpace(p.Address), + Status: p.Status, + IsDraft: p.IsDraft, + Remark: p.Remark, + InvoiceTitle: strings.TrimSpace(p.InvoiceTitle), + TaxNumber: strings.TrimSpace(p.TaxNumber), + BankName: strings.TrimSpace(p.BankName), + BankAccount: strings.TrimSpace(p.BankAccount), + RegisteredAddress: strings.TrimSpace(p.RegisteredAddress), + RegisteredPhone: strings.TrimSpace(p.RegisteredPhone), + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + + if p.RegisterTime != "" { + if t, err := time.ParseInLocation("2006-01-02", p.RegisterTime, time.Local); err == nil { + supplier.RegisterTime = &t + } + } + if p.ExpireTime != "" { + if t, err := time.ParseInLocation("2006-01-02", p.ExpireTime, time.Local); err == nil { + supplier.ExpireTime = &t + } + } + if p.EstablishDate != "" { + if t, err := time.ParseInLocation("2006-01-02", p.EstablishDate, time.Local); err == nil { + supplier.EstablishDate = &t + } + } + if supplier.SupplierLevel == "" { + supplier.SupplierLevel = "3" + } + if supplier.Status == "" { + supplier.Status = "1" + } + + id, err := models.Orm.Insert(&supplier) + if err != nil { + c.supplierJsonErr(500, 500, "创建失败: "+err.Error()) + return + } + + c.supplierOk(map[string]interface{}{"id": id}) +} + +// Update PUT /backend/erp/supplier/:id +func (c *BackendErpSupplierController) Update() { + claims, err := c.supplierClaims() + if err != nil { + c.supplierJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.supplierJsonErr(400, 400, "无效的ID") + return + } + + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.supplierJsonErr(400, 400, "参数错误") + return + } + var p supplierFormPayload + if err := json.Unmarshal(raw, &p); err != nil { + c.supplierJsonErr(400, 400, "参数错误") + return + } + + var supplier models.TenantCrmSupplier + err = models.Orm.QueryTable(new(models.TenantCrmSupplier)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + One(&supplier) + if err != nil { + c.supplierJsonErr(404, 404, "供应商未找到") + return + } + + supplier.SupplierName = strings.TrimSpace(p.SupplierName) + supplier.SupplierType = p.SupplierType + supplier.ContactPerson = strings.TrimSpace(p.ContactPerson) + supplier.ContactPhone = strings.TrimSpace(p.ContactPhone) + supplier.ContactEmail = strings.TrimSpace(p.ContactEmail) + supplier.SupplierLevel = p.SupplierLevel + supplier.Industry = strings.TrimSpace(p.Industry) + supplier.RegisteredCapital = strings.TrimSpace(p.RegisteredCapital) + supplier.PaidCapital = strings.TrimSpace(p.PaidCapital) + supplier.AdministrativeDivision = strings.TrimSpace(p.AdministrativeDivision) + supplier.EnterpriseType = strings.TrimSpace(p.EnterpriseType) + supplier.TaxpayerQualification = strings.TrimSpace(p.TaxpayerQualification) + supplier.BusinessScope = strings.TrimSpace(p.BusinessScope) + supplier.Address = strings.TrimSpace(p.Address) + supplier.Status = p.Status + supplier.IsDraft = p.IsDraft + supplier.Remark = p.Remark + supplier.InvoiceTitle = strings.TrimSpace(p.InvoiceTitle) + supplier.TaxNumber = strings.TrimSpace(p.TaxNumber) + supplier.BankName = strings.TrimSpace(p.BankName) + supplier.BankAccount = strings.TrimSpace(p.BankAccount) + supplier.RegisteredAddress = strings.TrimSpace(p.RegisteredAddress) + supplier.RegisteredPhone = strings.TrimSpace(p.RegisteredPhone) + supplier.UpdateTime = time.Now() + + if p.RegisterTime != "" { + if t, err := time.ParseInLocation("2006-01-02", p.RegisterTime, time.Local); err == nil { + supplier.RegisterTime = &t + } + } else { + supplier.RegisterTime = nil + } + if p.ExpireTime != "" { + if t, err := time.ParseInLocation("2006-01-02", p.ExpireTime, time.Local); err == nil { + supplier.ExpireTime = &t + } + } else { + supplier.ExpireTime = nil + } + if p.EstablishDate != "" { + if t, err := time.ParseInLocation("2006-01-02", p.EstablishDate, time.Local); err == nil { + supplier.EstablishDate = &t + } + } else { + supplier.EstablishDate = nil + } + + _, err = models.Orm.Update(&supplier) + if err != nil { + c.supplierJsonErr(500, 500, "更新失败: "+err.Error()) + return + } + + c.supplierOk(nil) +} + +// Delete DELETE /backend/erp/supplier/:id +func (c *BackendErpSupplierController) Delete() { + claims, err := c.supplierClaims() + if err != nil { + c.supplierJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.supplierJsonErr(400, 400, "无效的ID") + return + } + + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmSupplier)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Update(map[string]interface{}{ + "delete_time": now, + "update_time": now, + }) + if err != nil { + c.supplierJsonErr(500, 500, "删除失败: "+err.Error()) + return + } + + c.supplierOk(nil) +} + +// ToggleStatus POST /backend/erp/supplier/:id/status +func (c *BackendErpSupplierController) ToggleStatus() { + claims, err := c.supplierClaims() + if err != nil { + c.supplierJsonErr(401, 401, err.Error()) + return + } + + idStr := c.Ctx.Input.Param(":id") + id, _ := strconv.ParseUint(idStr, 10, 64) + if id == 0 { + c.supplierJsonErr(400, 400, "无效的ID") + return + } + + raw, _ := io.ReadAll(c.Ctx.Request.Body) + var p struct { + Status string `json:"status"` + } + _ = json.Unmarshal(raw, &p) + + _, err = models.Orm.QueryTable(new(models.TenantCrmSupplier)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Update(map[string]interface{}{ + "status": p.Status, + "update_time": time.Now(), + }) + if err != nil { + c.supplierJsonErr(500, 500, "操作失败: "+err.Error()) + return + } + + c.supplierOk(nil) +} diff --git a/go/controllers/backend_notify_config.go b/go/controllers/backend_notify_config.go new file mode 100644 index 0000000..1071561 --- /dev/null +++ b/go/controllers/backend_notify_config.go @@ -0,0 +1,136 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + beego "github.com/beego/beego/v2/server/web" +) + +// BackendNotifyConfigController 租户用户通知配置控制器 +type BackendNotifyConfigController struct { + beego.Controller +} + +func (c *BackendNotifyConfigController) ncClaims() (*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 *BackendNotifyConfigController) ncJsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *BackendNotifyConfigController) ncOk(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +type notifyConfigPayload struct { + Email string `json:"email"` + Phone string `json:"phone"` + BarkDeviceKey string `json:"bark_device_key"` + BarkServerURL string `json:"bark_server_url"` +} + +// GetConfig GET /backend/notify-config +// 获取当前登录用户的通知配置 +func (c *BackendNotifyConfigController) GetConfig() { + claims, err := c.ncClaims() + if err != nil { + c.ncJsonErr(401, 401, err.Error()) + return + } + + cfg := models.GetBackendUserNotifyConfig(uint64(claims.TenantId), uint64(claims.UserID)) + if cfg == nil { + // 未配置时返回空结构,前端可以直接展示空表单 + cfg = &models.BackendUserNotifyConfig{} + } + c.ncOk(cfg) +} + +// UpdateConfig PUT /backend/notify-config +// 更新当前登录用户的通知配置(不存在则创建) +func (c *BackendNotifyConfigController) UpdateConfig() { + claims, err := c.ncClaims() + if err != nil { + c.ncJsonErr(401, 401, err.Error()) + return + } + + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + c.ncJsonErr(400, 400, "参数错误") + return + } + var p notifyConfigPayload + if err := json.Unmarshal(raw, &p); err != nil { + c.ncJsonErr(400, 400, "参数错误") + return + } + + tid := uint64(claims.TenantId) + uid := uint64(claims.UserID) + now := time.Now() + + cfg := models.GetBackendUserNotifyConfig(tid, uid) + if cfg == nil { + cfg = &models.BackendUserNotifyConfig{ + Tid: tid, + UserID: uid, + CreateTime: now, + } + } + + cfg.Email = ncStrPtr(p.Email) + cfg.Phone = ncStrPtr(p.Phone) + cfg.BarkDeviceKey = ncStrPtr(p.BarkDeviceKey) + cfg.BarkServerURL = ncStrPtr(p.BarkServerURL) + cfg.UpdateTime = now + + if cfg.ID == 0 { + if _, err := models.Orm.Insert(cfg); err != nil { + c.ncJsonErr(500, 500, "保存失败: "+err.Error()) + return + } + } else { + if _, err := models.Orm.Update(cfg, "Email", "Phone", "BarkDeviceKey", "BarkServerURL", "UpdateTime"); err != nil { + c.ncJsonErr(500, 500, "更新失败: "+err.Error()) + return + } + } + + c.ncOk(nil) +} + +// ncStrPtr 字符串转指针,空字符串返回 nil +func ncStrPtr(s string) *string { + s = strings.TrimSpace(s) + if s == "" { + return nil + } + return &s +} diff --git a/go/models/backend_user_notify_config.go b/go/models/backend_user_notify_config.go new file mode 100644 index 0000000..bf9c041 --- /dev/null +++ b/go/models/backend_user_notify_config.go @@ -0,0 +1,32 @@ +package models + +import "time" + +// BackendUserNotifyConfig 租户用户通知配置表: yz_backend_user_notify_config +// 每个用户一条记录,保存自己的接收渠道配置(邮箱/手机号/Bark) +type BackendUserNotifyConfig struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid)" json:"tid"` + UserID uint64 `orm:"column(user_id)" json:"user_id"` + Email *string `orm:"column(email);size(128);null" json:"email"` + Phone *string `orm:"column(phone);size(20);null" json:"phone"` + BarkDeviceKey *string `orm:"column(bark_device_key);size(128);null" json:"bark_device_key"` + BarkServerURL *string `orm:"column(bark_server_url);size(255);null" json:"bark_server_url"` // 为空用默认 https://api.day.app + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` +} + +func (m *BackendUserNotifyConfig) TableName() string { + return "yz_backend_user_notify_config" +} + +// GetBackendUserNotifyConfig 按 tid+user_id 查询配置,不存在返回 nil +func GetBackendUserNotifyConfig(tid, userID uint64) *BackendUserNotifyConfig { + var cfg BackendUserNotifyConfig + err := Orm.QueryTable(new(BackendUserNotifyConfig)). + Filter("tid", tid).Filter("user_id", userID).One(&cfg) + if err != nil { + return nil + } + return &cfg +} diff --git a/go/models/contact.go b/go/models/contact.go index 3dfdd33..5eb9b10 100644 --- a/go/models/contact.go +++ b/go/models/contact.go @@ -32,5 +32,5 @@ type BackendErpContact struct { } func (m *BackendErpContact) TableName() string { - return "yz_backend_contact" + return "yz_backend_erp_contact" } diff --git a/go/models/erp_account_set.go b/go/models/erp_account_set.go new file mode 100644 index 0000000..050bbaa --- /dev/null +++ b/go/models/erp_account_set.go @@ -0,0 +1,27 @@ +package models + +import "time" + +// ErpAccountSet ERP账套管理表: yz_erp_account_set +type ErpAccountSet struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + SetCode string `orm:"column(set_code);size(50)" json:"set_code"` + SetName string `orm:"column(set_name);size(100)" json:"set_name"` + CompanyName string `orm:"column(company_name);size(100)" json:"company_name"` + CompanyTaxNo string `orm:"column(company_tax_no);size(50)" json:"company_tax_no"` + StartYear int `orm:"column(start_year);default(2024)" json:"start_year"` + StartMonth int `orm:"column(start_month);default(1)" json:"start_month"` + AccountingStandard string `orm:"column(accounting_standard);size(20);default(1)" json:"accounting_standard"` + Currency string `orm:"column(currency);size(10);default(CNY)" json:"currency"` + TaxRate float64 `orm:"column(tax_rate);digits(5);decimals(2);default(13.00)" json:"tax_rate"` + Status string `orm:"column(status);size(20);default(1)" json:"status"` + Remark string `orm:"column(remark);type(text);null" json:"remark"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *ErpAccountSet) TableName() string { + return "yz_backend_erp_account_set" +} diff --git a/go/models/erp_company_contact.go b/go/models/erp_company_contact.go new file mode 100644 index 0000000..709360a --- /dev/null +++ b/go/models/erp_company_contact.go @@ -0,0 +1,32 @@ +package models + +import "time" + +// ErpCompanyContact 客户/供应商联系人表 +type ErpCompanyContact struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + CompanyType string `orm:"column(company_type);size(20)" json:"company_type"` // customer / supplier + CompanyID uint64 `orm:"column(company_id)" json:"company_id"` + Name string `orm:"column(name);size(50)" json:"name"` + Gender int8 `orm:"column(gender);default(0)" json:"gender"` // 0-未知 1-男 2-女 + Phone string `orm:"column(phone);size(20)" json:"phone"` + Mobiles string `orm:"column(mobiles);type(text);null" json:"mobiles"` // JSON数组,最多5个 + Wechat string `orm:"column(wechat);size(50)" json:"wechat"` + QQ string `orm:"column(qq);size(50)" json:"qq"` + Dingtalk string `orm:"column(dingtalk);size(50)" json:"dingtalk"` + Email string `orm:"column(email);size(100)" json:"email"` + Department string `orm:"column(department);size(50)" json:"department"` + Position string `orm:"column(position);size(50)" json:"position"` + HomeAddress string `orm:"column(home_address);size(255)" json:"home_address"` + IsPrimary int8 `orm:"column(is_primary);default(0)" json:"is_primary"` // 0-否 1-是 + Status int8 `orm:"column(status);default(1)" json:"status"` // 0-离职 1-在职 + Remark string `orm:"column(remark);type(text);null" json:"remark"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *ErpCompanyContact) TableName() string { + return "yz_backend_erp_company_contact" +} diff --git a/go/models/erp_normal_setting.go b/go/models/erp_normal_setting.go new file mode 100644 index 0000000..a3e469d --- /dev/null +++ b/go/models/erp_normal_setting.go @@ -0,0 +1,48 @@ +package models + +import "time" + +// ErpNormalSetting ERP基础设置表(键值对方式): yz_erp_normalsettings +// 每个租户可配置多条基础参数,按 config_group 分组展示 +type ErpNormalSetting struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + ConfigKey string `orm:"column(config_key);size(100)" json:"config_key"` + ConfigValue string `orm:"column(config_value);type(text);null" json:"config_value"` + ConfigName string `orm:"column(config_name);size(100)" json:"config_name"` + ConfigGroup string `orm:"column(config_group);size(50)" json:"config_group"` + ConfigType string `orm:"column(config_type);size(20);default(input)" json:"config_type"` // input/select/number/textarea/switch + ConfigOptions string `orm:"column(config_options);type(text);null" json:"config_options"` // select 类型的选项,JSON格式 + SortOrder int `orm:"column(sort_order);default(0)" json:"sort_order"` + Status int8 `orm:"column(status);default(1)" json:"status"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` +} + +func (m *ErpNormalSetting) TableName() string { + return "yz_backend_erp_normalsettings" +} + +// GetErpNormalSettingsByTenant 获取租户所有启用的基础设置 +func GetErpNormalSettingsByTenant(tenantID string) ([]ErpNormalSetting, error) { + var list []ErpNormalSetting + _, err := Orm.QueryTable(new(ErpNormalSetting)). + Filter("tenant_id", tenantID). + Filter("status", 1). + OrderBy("config_group", "sort_order", "id"). + All(&list) + return list, err +} + +// GetErpNormalSettingByKey 按租户+key获取配置 +func GetErpNormalSettingByKey(tenantID, key string) *ErpNormalSetting { + var setting ErpNormalSetting + err := Orm.QueryTable(new(ErpNormalSetting)). + Filter("tenant_id", tenantID). + Filter("config_key", key). + One(&setting) + if err != nil { + return nil + } + return &setting +} diff --git a/go/models/init.go b/go/models/init.go index 9a51ccc..5b3273d 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -71,6 +71,12 @@ func Init(_ string) { new(PlatformAgentApi), new(PlatformPasswordStore), new(BackendPasswordStore), + new(BackendUserNotifyConfig), + new(TenantCrmCustomer), + new(TenantCrmSupplier), + new(ErpAccountSet), + new(ErpNormalSetting), + new(ErpCompanyContact), new(CmsArticleCategory), new(CmsArticle), diff --git a/go/models/tenant_crm_customer.go b/go/models/tenant_crm_customer.go new file mode 100644 index 0000000..2526d77 --- /dev/null +++ b/go/models/tenant_crm_customer.go @@ -0,0 +1,42 @@ +package models + +import "time" + +// TenantCrmCustomer 客户管理表: yz_tenant_crm_customer +type TenantCrmCustomer struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + CustomerName string `orm:"column(customer_name);size(100)" json:"customer_name"` + CustomerType string `orm:"column(customer_type);size(20)" json:"customer_type"` + ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"` + ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"` + ContactEmail string `orm:"column(contact_email);size(100)" json:"contact_email"` + CustomerLevel string `orm:"column(customer_level);size(20);default(3)" json:"customer_level"` + Industry string `orm:"column(industry);size(50)" json:"industry"` + RegisteredCapital string `orm:"column(registered_capital);size(50)" json:"registered_capital"` + PaidCapital string `orm:"column(paid_capital);size(50)" json:"paid_capital"` + EstablishDate *time.Time `orm:"column(establish_date);type(date);null" json:"establish_date"` + AdministrativeDivision string `orm:"column(administrative_division);size(100)" json:"administrative_division"` + EnterpriseType string `orm:"column(enterprise_type);size(50)" json:"enterprise_type"` + TaxpayerQualification string `orm:"column(taxpayer_qualification);size(50)" json:"taxpayer_qualification"` + BusinessScope string `orm:"column(business_scope);type(text);null" json:"business_scope"` + Address string `orm:"column(address);size(255)" json:"address"` + RegisterTime *time.Time `orm:"column(register_time);type(date);null" json:"register_time"` + ExpireTime *time.Time `orm:"column(expire_time);type(date);null" json:"expire_time"` + Status string `orm:"column(status);size(20);default(1)" json:"status"` + IsDraft int8 `orm:"column(is_draft);default(0)" json:"is_draft"` // 0-正式 1-草稿 + Remark string `orm:"column(remark);type(text);null" json:"remark"` + InvoiceTitle string `orm:"column(invoice_title);size(100)" json:"invoice_title"` + TaxNumber string `orm:"column(tax_number);size(50)" json:"tax_number"` + BankName string `orm:"column(bank_name);size(100)" json:"bank_name"` + BankAccount string `orm:"column(bank_account);size(50)" json:"bank_account"` + RegisteredAddress string `orm:"column(registered_address);size(255)" json:"registered_address"` + RegisteredPhone string `orm:"column(registered_phone);size(50)" json:"registered_phone"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmCustomer) TableName() string { + return "yz_backend_erp_customer" +} diff --git a/go/models/tenant_crm_supplier.go b/go/models/tenant_crm_supplier.go new file mode 100644 index 0000000..c3cd473 --- /dev/null +++ b/go/models/tenant_crm_supplier.go @@ -0,0 +1,42 @@ +package models + +import "time" + +// TenantCrmSupplier 供应商管理表: yz_tenant_crm_supplier +type TenantCrmSupplier struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + SupplierName string `orm:"column(supplier_name);size(100)" json:"supplier_name"` + SupplierType string `orm:"column(supplier_type);size(20)" json:"supplier_type"` + ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"` + ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"` + ContactEmail string `orm:"column(contact_email);size(100)" json:"contact_email"` + SupplierLevel string `orm:"column(supplier_level);size(20);default(3)" json:"supplier_level"` + Industry string `orm:"column(industry);size(50)" json:"industry"` + RegisteredCapital string `orm:"column(registered_capital);size(50)" json:"registered_capital"` + PaidCapital string `orm:"column(paid_capital);size(50)" json:"paid_capital"` + EstablishDate *time.Time `orm:"column(establish_date);type(date);null" json:"establish_date"` + AdministrativeDivision string `orm:"column(administrative_division);size(100)" json:"administrative_division"` + EnterpriseType string `orm:"column(enterprise_type);size(50)" json:"enterprise_type"` + TaxpayerQualification string `orm:"column(taxpayer_qualification);size(50)" json:"taxpayer_qualification"` + BusinessScope string `orm:"column(business_scope);type(text);null" json:"business_scope"` + Address string `orm:"column(address);size(255)" json:"address"` + RegisterTime *time.Time `orm:"column(register_time);type(date);null" json:"register_time"` + ExpireTime *time.Time `orm:"column(expire_time);type(date);null" json:"expire_time"` + Status string `orm:"column(status);size(20);default(1)" json:"status"` + IsDraft int8 `orm:"column(is_draft);default(0)" json:"is_draft"` // 0-正式 1-草稿 + Remark string `orm:"column(remark);type(text);null" json:"remark"` + InvoiceTitle string `orm:"column(invoice_title);size(100)" json:"invoice_title"` + TaxNumber string `orm:"column(tax_number);size(50)" json:"tax_number"` + BankName string `orm:"column(bank_name);size(100)" json:"bank_name"` + BankAccount string `orm:"column(bank_account);size(50)" json:"bank_account"` + RegisteredAddress string `orm:"column(registered_address);size(255)" json:"registered_address"` + RegisteredPhone string `orm:"column(registered_phone);size(50)" json:"registered_phone"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *TenantCrmSupplier) TableName() string { + return "yz_backend_erp_supplier" +} diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index d6c3f5f..b733135 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -1,4 +1,4 @@ -package backend +package backend import ( "server/controllers" @@ -244,6 +244,9 @@ func RegisterAuthRoutes() { beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder") beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder") + // 用户通知配置(每个用户自己的邮箱/手机号/Bark) + beego.Router("/backend/notify-config", &controllers.BackendNotifyConfigController{}, "get:GetConfig;put:UpdateConfig") + // OA 薪酬管理(薪酬方案 / 月度薪资单) beego.Router("/backend/oa/compensation/employees", &controllers.BackendOaCompensationController{}, "get:Employees") beego.Router("/backend/oa/compensation/dashboard", &controllers.BackendOaCompensationController{}, "get:Dashboard") @@ -308,4 +311,31 @@ func registerOrganizationRoutes(module string) { beego.Router(prefix+"/exportOrganization", &controllers.BackendOrganizationController{}, "get:ExportOrganization") beego.Router(prefix+"/importOrganization", &controllers.BackendOrganizationController{}, "post:ImportOrganization") beego.Router(prefix+"/organizationImportTemplate", &controllers.BackendOrganizationController{}, "get:GetImportTemplate") + + // ERP客户管理 + beego.Router("/backend/erp/customer/list", &controllers.BackendErpCustomerController{}, "get:List") + beego.Router("/backend/erp/customer", &controllers.BackendErpCustomerController{}, "post:Create") + beego.Router("/backend/erp/customer/:id", &controllers.BackendErpCustomerController{}, "get:Detail;put:Update;delete:Delete") + beego.Router("/backend/erp/customer/:id/status", &controllers.BackendErpCustomerController{}, "post:ToggleStatus") + + // ERP供应商管理 + beego.Router("/backend/erp/supplier/list", &controllers.BackendErpSupplierController{}, "get:List") + beego.Router("/backend/erp/supplier", &controllers.BackendErpSupplierController{}, "post:Create") + beego.Router("/backend/erp/supplier/:id", &controllers.BackendErpSupplierController{}, "get:Detail;put:Update;delete:Delete") + beego.Router("/backend/erp/supplier/:id/status", &controllers.BackendErpSupplierController{}, "post:ToggleStatus") + + // ERP账套管理 + beego.Router("/backend/erp/account-set/list", &controllers.BackendErpAccountSetController{}, "get:List") + beego.Router("/backend/erp/account-set", &controllers.BackendErpAccountSetController{}, "post:Create") + beego.Router("/backend/erp/account-set/:id", &controllers.BackendErpAccountSetController{}, "get:Detail;put:Update;delete:Delete") + beego.Router("/backend/erp/account-set/:id/status", &controllers.BackendErpAccountSetController{}, "post:ToggleStatus") + + // ERP基础设置 + beego.Router("/backend/erp/normal-settings", &controllers.BackendErpNormalSettingController{}, "get:List;put:Save") + beego.Router("/backend/erp/normal-settings/reset", &controllers.BackendErpNormalSettingController{}, "post:Reset") + + // ERP客户/供应商联系人 + beego.Router("/backend/erp/company-contact/list", &controllers.BackendErpCompanyContactController{}, "get:List") + beego.Router("/backend/erp/company-contact", &controllers.BackendErpCompanyContactController{}, "post:Create") + beego.Router("/backend/erp/company-contact/:id", &controllers.BackendErpCompanyContactController{}, "put:Update;delete:Delete") } diff --git a/go/services/reminder_scheduler.go b/go/services/reminder_scheduler.go index a724264..da18279 100644 --- a/go/services/reminder_scheduler.go +++ b/go/services/reminder_scheduler.go @@ -28,6 +28,90 @@ type ReminderSender interface { Send(ctx context.Context, reminder ReminderData, title, content string) (success bool, err error) } +// resolveReceiverTarget 统一解析接收目标 +// 优先级(从高到低): +// 1. 单条提醒指定的 ReceiverTarget(创建提醒时手动填写) +// 2. backend 用户通知配置表(yz_backend_user_notify_config) +// 3. 用户基础信息表(backend 查 SystemTenantUser,platform 查 AdminUser) +// +// channel: SMS / EMAIL / BARK +// data.Tid > 0 表示 backend 租户用户,Tid == 0 表示 platform 管理员 +func resolveReceiverTarget(channel string, data ReminderData) string { + // 1. 单条提醒指定的目标最高优先 + if data.ReceiverTarget != nil && *data.ReceiverTarget != "" { + return *data.ReceiverTarget + } + + // 2. backend 用户查用户通知配置表 + if data.Tid > 0 { + cfg := models.GetBackendUserNotifyConfig(uint64(data.Tid), data.ReceiverUserID) + if cfg != nil { + switch channel { + case "SMS": + if cfg.Phone != nil && *cfg.Phone != "" { + return *cfg.Phone + } + case "EMAIL": + if cfg.Email != nil && *cfg.Email != "" { + return *cfg.Email + } + case "BARK": + if cfg.BarkDeviceKey != nil && *cfg.BarkDeviceKey != "" { + return *cfg.BarkDeviceKey + } + } + } + } + + // 3. 最后 fallback 到用户基础信息表 + if data.Tid > 0 { + // backend 租户用户 + var user models.SystemTenantUser + if err := models.Orm.QueryTable(new(models.SystemTenantUser)). + Filter("id", data.ReceiverUserID).One(&user); err == nil { + switch channel { + case "SMS": + if user.Phone != nil && *user.Phone != "" { + return *user.Phone + } + case "EMAIL": + if user.Email != nil && *user.Email != "" { + return *user.Email + } + } + } + } else { + // platform 管理员 + var user models.AdminUser + if err := models.Orm.QueryTable(new(models.AdminUser)). + Filter("id", data.ReceiverUserID).One(&user); err == nil { + switch channel { + case "SMS": + if user.Phone != nil && *user.Phone != "" { + return *user.Phone + } + case "EMAIL": + if user.Email != nil && *user.Email != "" { + return *user.Email + } + } + } + } + return "" +} + +// resolveBarkServerURL 解析 Bark 服务端地址 +// backend 用户优先用自己配置的 server URL,否则用平台默认配置 +func resolveBarkServerURL(data ReminderData) string { + if data.Tid > 0 { + cfg := models.GetBackendUserNotifyConfig(uint64(data.Tid), data.ReceiverUserID) + if cfg != nil && cfg.BarkServerURL != nil && *cfg.BarkServerURL != "" { + return *cfg.BarkServerURL + } + } + return models.GetPlatformSettingValue("bark_server_url", "https://api.day.app") +} + // SMSSender 短信发送实现 type SMSSender struct{} @@ -36,17 +120,10 @@ func (s *SMSSender) Send(ctx context.Context, reminder ReminderData, title, cont if err != nil { return false, err } - phone := "" - if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" { - phone = *reminder.ReceiverTarget - } else { - var user models.AdminUser - if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Phone != nil { - phone = *user.Phone - } - } + + phone := resolveReceiverTarget("SMS", reminder) if phone == "" { - return false, fmt.Errorf("未配置手机号") + return false, fmt.Errorf("未配置手机号(请在通知设置中配置,或在创建提醒时指定)") } enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks" @@ -91,17 +168,9 @@ func (s *EmailSender) Send(ctx context.Context, reminder ReminderData, title, co return false, fmt.Errorf("未配置系统邮箱") } - toEmail := "" - if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" { - toEmail = *reminder.ReceiverTarget - } else { - var user models.AdminUser - if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Email != nil { - toEmail = *user.Email - } - } + toEmail := resolveReceiverTarget("EMAIL", reminder) if toEmail == "" { - return false, fmt.Errorf("未配置收件邮箱") + return false, fmt.Errorf("未配置收件邮箱(请在通知设置中配置,或在创建提醒时指定)") } sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn") @@ -156,17 +225,12 @@ func (s *EmailSender) Send(ctx context.Context, reminder ReminderData, title, co type BarkSender struct{} func (s *BarkSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) { - deviceKey := "" - if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" { - deviceKey = *reminder.ReceiverTarget - } else { - deviceKey = models.GetPlatformSettingValue("bark_device_key", "") - } + deviceKey := resolveReceiverTarget("BARK", reminder) if deviceKey == "" { - return false, fmt.Errorf("Bark 设备 Key 未配置") + return false, fmt.Errorf("Bark 设备 Key 未配置(请在通知设置中配置,或在创建提醒时指定)") } - serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app") + serverURL := resolveBarkServerURL(reminder) sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn") ackToken := "" if reminder.AckToken != nil {
配置ERP系统基础参数,包括公司信息、税务、单据、库存、财务等
管理企业账套信息,支持多账套切换与独立核算
管理企业客户信息,支持多维度筛选与全生命周期追踪
管理企业供应商信息,支持多维度筛选与全生命周期追踪
选择要测试的提醒渠道,将立即发送一条测试提醒到对应配置。