From e4b95eb05e6b57628e0ffa749babda123771faff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B9=B4=E7=B3=95=E5=B4=BD=E5=B4=BD?= Date: Sat, 18 Jul 2026 05:58:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/00-global.mdc | 12 + .cursor/rules/10-frontend.mdc | 17 + .cursor/rules/20-backend-goframe.mdc | 17 + .gitattributes | 1 + .gitignore | 19 + AGENTS.md | 44 ++ Makefile | 7 + README.MD | 4 + api/admin/v1/admin.go | 176 ++++++++ api/hello/hello.go | 15 + api/hello/v1/hello.go | 12 + api/lead/v1/lead.go | 24 ++ api/order/v1/order.go | 30 ++ api/payment/v1/payment.go | 11 + api/pricing/v1/pricing.go | 17 + api/site/v1/site.go | 24 ++ go.mod | 35 ++ go.sum | 81 ++++ hack/config.yaml | 13 + hack/hack-cli.mk | 18 + hack/hack.mk | 75 ++++ internal/cmd/cmd.go | 43 ++ internal/consts/consts.go | 1 + internal/controller/admin/admin.go | 9 + internal/controller/admin/admin_v1.go | 111 +++++ internal/controller/hello/hello.go | 5 + internal/controller/hello/hello_new.go | 15 + internal/controller/hello/hello_v1_hello.go | 13 + internal/controller/lead/lead.go | 9 + internal/controller/lead/lead_v1.go | 27 ++ internal/controller/order/order.go | 9 + internal/controller/order/order_v1.go | 37 ++ internal/controller/payment/payment.go | 9 + internal/controller/payment/payment_v1.go | 21 + internal/controller/pricing/pricing.go | 9 + internal/controller/pricing/pricing_v1.go | 21 + internal/controller/site/site.go | 9 + internal/controller/site/site_v1_home.go | 27 ++ internal/dao/.gitkeep | 0 internal/model/.gitkeep | 0 internal/model/do/.gitkeep | 0 internal/model/entity/.gitkeep | 0 internal/model/website.go | 183 +++++++++ internal/response/response.go | 38 ++ internal/service/.gitkeep | 0 internal/service/admin.go | 386 ++++++++++++++++++ internal/service/lead.go | 33 ++ internal/service/order.go | 50 +++ internal/service/pricing.go | 28 ++ internal/service/site.go | 59 +++ main.go | 11 + .../deploy/kustomize/base/deployment.yaml | 21 + .../deploy/kustomize/base/kustomization.yaml | 8 + manifest/deploy/kustomize/base/service.yaml | 12 + .../kustomize/overlays/develop/configmap.yaml | 14 + .../overlays/develop/deployment.yaml | 10 + .../overlays/develop/kustomization.yaml | 14 + manifest/docker/Dockerfile | 16 + manifest/docker/docker.sh | 8 + manifest/i18n/.gitkeep | 0 manifest/protobuf/.keep-if-necessary | 0 manifest/sql/xk_of_schema.sql | 371 +++++++++++++++++ resource/public/html/.gitkeep | 0 resource/public/plugin/.gitkeep | 0 resource/public/resource/css/.gitkeep | 0 resource/public/resource/image/.gitkeep | 0 resource/public/resource/js/.gitkeep | 0 resource/template/.gitkeep | 0 utility/.gitkeep | 0 69 files changed, 2289 insertions(+) create mode 100644 .cursor/rules/00-global.mdc create mode 100644 .cursor/rules/10-frontend.mdc create mode 100644 .cursor/rules/20-backend-goframe.mdc create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Makefile create mode 100644 README.MD create mode 100644 api/admin/v1/admin.go create mode 100644 api/hello/hello.go create mode 100644 api/hello/v1/hello.go create mode 100644 api/lead/v1/lead.go create mode 100644 api/order/v1/order.go create mode 100644 api/payment/v1/payment.go create mode 100644 api/pricing/v1/pricing.go create mode 100644 api/site/v1/site.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/config.yaml create mode 100644 hack/hack-cli.mk create mode 100644 hack/hack.mk create mode 100644 internal/cmd/cmd.go create mode 100644 internal/consts/consts.go create mode 100644 internal/controller/admin/admin.go create mode 100644 internal/controller/admin/admin_v1.go create mode 100644 internal/controller/hello/hello.go create mode 100644 internal/controller/hello/hello_new.go create mode 100644 internal/controller/hello/hello_v1_hello.go create mode 100644 internal/controller/lead/lead.go create mode 100644 internal/controller/lead/lead_v1.go create mode 100644 internal/controller/order/order.go create mode 100644 internal/controller/order/order_v1.go create mode 100644 internal/controller/payment/payment.go create mode 100644 internal/controller/payment/payment_v1.go create mode 100644 internal/controller/pricing/pricing.go create mode 100644 internal/controller/pricing/pricing_v1.go create mode 100644 internal/controller/site/site.go create mode 100644 internal/controller/site/site_v1_home.go create mode 100644 internal/dao/.gitkeep create mode 100644 internal/model/.gitkeep create mode 100644 internal/model/do/.gitkeep create mode 100644 internal/model/entity/.gitkeep create mode 100644 internal/model/website.go create mode 100644 internal/response/response.go create mode 100644 internal/service/.gitkeep create mode 100644 internal/service/admin.go create mode 100644 internal/service/lead.go create mode 100644 internal/service/order.go create mode 100644 internal/service/pricing.go create mode 100644 internal/service/site.go create mode 100644 main.go create mode 100644 manifest/deploy/kustomize/base/deployment.yaml create mode 100644 manifest/deploy/kustomize/base/kustomization.yaml create mode 100644 manifest/deploy/kustomize/base/service.yaml create mode 100644 manifest/deploy/kustomize/overlays/develop/configmap.yaml create mode 100644 manifest/deploy/kustomize/overlays/develop/deployment.yaml create mode 100644 manifest/deploy/kustomize/overlays/develop/kustomization.yaml create mode 100644 manifest/docker/Dockerfile create mode 100644 manifest/docker/docker.sh create mode 100644 manifest/i18n/.gitkeep create mode 100644 manifest/protobuf/.keep-if-necessary create mode 100644 manifest/sql/xk_of_schema.sql create mode 100644 resource/public/html/.gitkeep create mode 100644 resource/public/plugin/.gitkeep create mode 100644 resource/public/resource/css/.gitkeep create mode 100644 resource/public/resource/image/.gitkeep create mode 100644 resource/public/resource/js/.gitkeep create mode 100644 resource/template/.gitkeep create mode 100644 utility/.gitkeep diff --git a/.cursor/rules/00-global.mdc b/.cursor/rules/00-global.mdc new file mode 100644 index 0000000..9f96d37 --- /dev/null +++ b/.cursor/rules/00-global.mdc @@ -0,0 +1,12 @@ +--- +description: 萧康云医项目全局编码规则 +alwaysApply: true +--- + +# 全局规则 + +- 代码、包、方法、结构体、接口、关键业务分支、复杂判断、关键配置均需要编写清晰的中文注释。 +- 注释要说明业务意图、输入输出、边界条件和注意事项,避免只重复代码字面含义。 +- 模块职责要清晰,避免跨层调用、重复实现和无关逻辑混入。 +- 新增或修改功能时,优先遵循项目已有目录结构、命名方式、封装习惯和错误处理方式。 +- 公共能力应沉淀为可复用模块或组件,避免在多个页面、控制器或服务中复制粘贴。 diff --git a/.cursor/rules/10-frontend.mdc b/.cursor/rules/10-frontend.mdc new file mode 100644 index 0000000..3c87691 --- /dev/null +++ b/.cursor/rules/10-frontend.mdc @@ -0,0 +1,17 @@ +--- +description: 萧康云医官网前端设计与实现规则 +globs: xk-of-view/**/*,**/*.html,**/*.css,**/*.scss,**/*.less,**/*.vue,**/*.tsx,**/*.ts,**/*.jsx,**/*.js +alwaysApply: false +--- + +# 前端规则 + +- 项目名称为“萧康云医官网”,定位为面向诊所赋能业务的 SaaS 平台。 +- 产品端形态包含小程序患者端、医生端、门店老板端、推广员端,以及 PC 端管理或官网相关页面。 +- 页面设计要体现 SaaS 平台的专业、清晰、高效,优先服务信息扫描、业务转化和多角色理解。 +- 必须注意多端适配,兼容移动端、平板端和桌面端,不允许出现文字溢出、布局重叠、关键按钮不可触达等问题。 +- 必须提供暗色模式适配,颜色、阴影、边框、图标、图表和状态提示都要在明暗主题下保持可读。 +- 采用模块化、组件化设计,页面由清晰的业务模块和可复用 UI 组件组成。 +- 组件应保持职责单一,状态、事件、属性和插槽设计清楚,避免单个组件承担过多页面逻辑。 +- 表单、列表、筛选、弹窗、空状态、加载态、错误态、成功态等常见状态要完整设计。 +- 视觉风格要克制、现代、可信,适合医疗和诊所经营场景;避免过度营销化、装饰化或单一色系堆叠。 diff --git a/.cursor/rules/20-backend-goframe.mdc b/.cursor/rules/20-backend-goframe.mdc new file mode 100644 index 0000000..866d73c --- /dev/null +++ b/.cursor/rules/20-backend-goframe.mdc @@ -0,0 +1,17 @@ +--- +description: 萧康云医后端 GoFrame GF 分层开发规则 +globs: **/*.go,api/**/*,internal/**/*,manifest/**/*,resource/**/* +alwaysApply: false +--- + +# 后端规则 + +- 后端使用 GoFrame GF 框架,必须遵循 GF 的项目结构、路由注册、控制器、服务、模型、配置和错误处理规范。 +- 分层链路为:路由 -> 控制器 -> service 服务层 -> 模型层。 +- 路由层只负责请求路径、HTTP 方法、中间件和控制器绑定,不编写业务逻辑。 +- 控制器层负责获取请求信息、参数验证、用户信息解析、调用 service,并返回统一响应。 +- 控制器层不编写核心业务逻辑,不直接操作数据库,不绕过 service 调用模型。 +- service 层是业务逻辑层,负责业务流程编排、权限判断、事务控制、数据处理和外部服务调用。 +- 模型层只用于定义表结构、表关系、字段映射和数据承载,不编写业务逻辑。 +- 数据库访问、事务、缓存、队列、第三方接口等能力应按 GF 推荐方式封装,并放在合适的服务或基础设施模块中。 +- 参数校验、错误码、日志、上下文传递和用户身份解析要保持统一规范。 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1fbf887 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* linguist-language=GO \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18646b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +.buildpath +.hgignore.swp +.project +.orig +.swp +.idea/ +.settings/ +.vscode/ +bin/ +**/.DS_Store +gf +main +main.exe +output/ +manifest/output/ +temp/ +temp.yaml +bin +**/config/config.yaml \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5174810 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +# 项目协作规则 + +## 全局规则 + +- 代码、包、方法、结构体、接口、关键业务分支、复杂判断和关键配置都需要编写清晰的中文注释。 +- 注释要说明业务意图、输入输出、边界条件和注意事项,避免只重复代码字面含义。 +- 模块职责要清晰,避免跨层调用、重复实现和无关逻辑混入。 +- 新增或修改功能时,优先遵循项目已有目录结构、命名方式、封装习惯和错误处理方式。 +- 公共能力应沉淀为可复用模块或组件,避免在多个页面、控制器或服务中复制粘贴。 + +## 前端规则 + +- 项目名称为“萧康云医官网”,定位为面向诊所赋能业务的 SaaS 平台。 +- 产品端形态包含小程序患者端、医生端、门店老板端、推广员端,以及 PC 管理后台和官网相关页面。 +- 页面设计要体现 SaaS 平台的专业、清晰、高效,优先服务信息扫描、业务转化和多角色理解。 +- 必须注意多端适配,兼容移动端、平板端和桌面端,不允许出现文字溢出、布局重叠、关键按钮不可触达等问题。 +- 必须提供暗色模式适配;暗色模式以灰黑体系为主,文字、按钮、卡片、图表和状态提示都要保持可读。 +- 采用模块化、组件化设计,页面由清晰的业务模块和可复用 UI 组件组成。 +- 组件职责保持单一,状态、事件、属性和插槽设计清楚,避免单个组件承担过多页面逻辑。 +- 表单、列表、筛选、弹窗、空状态、加载状态、错误状态、成功状态等常见状态要完整设计。 +- 视觉风格要克制、现代、可信,适合医疗和诊所经营场景;避免过度营销化、装饰化或单一色系堆叠。 + +## 后端规则 + +- 后端使用 GoFrame GF 框架,必须遵循 GF 的项目结构、路由注册、控制器、服务、模型、配置和错误处理规范。 +- 分层链路固定为:路由 -> 控制器 -> service 服务层 -> 模型层。 +- 路由层只负责请求路径、HTTP 方法、中间件和控制器绑定,不编写业务逻辑。 +- 控制器层负责获取请求信息、参数验证、用户信息解析、调用 service,并返回统一响应。 +- 控制器层不编写核心业务逻辑,不直接操作数据库,不绕过 service 调用模型。 +- service 层是业务逻辑层,负责业务流程编排、权限判断、事务控制、数据处理和外部服务调用。 +- 模型层只用于定义表结构、表关系、字段映射和数据承载,不编写业务逻辑。 +- 数据库访问、事务、缓存、队列、第三方接口等能力应按 GF 推荐方式封装,并放在合适的服务或基础设施模块中。 +- 参数校验、错误码、日志、上下文传递和用户身份解析要保持统一规范。 +- 不准使用数据库迁移自动删除或破坏已有数据,结构调整必须提供可审查的 SQL。 + +## API 响应与字段命名规则 + +- 所有 HTTP API 的 HTTP 状态码统一返回 200,不用 4xx/5xx 直接表达业务失败;前端必须通过响应体 `code` 判断成功或失败。 +- 统一响应结构固定为 `{ "code": 0, "message": "ok", "result": ... }`。 +- 业务错误返回 `code != 0`,`message` 必须给出清晰中文错误信息,`result` 默认返回 `null`。 +- 控制器返回错误时必须调用统一响应封装,禁止出现无错误信息的 500、空响应或框架默认错误页。 +- JSON 请求字段和响应字段统一使用下划线命名 `snake_case`,例如 `order_no`、`tenant_name`、`contact_name`、`plan_code`、`billing_cycle`、`menu_layout`、`global_layout`、`final_layout`。 +- Go 结构体字段可以保持 Go 命名规范,但 `json` tag 必须写成 `snake_case`;map 返回值的 key 也必须使用 `snake_case`。 +- 使用 GF 时避免统一响应被二次包装;如果控制器已经写出 `{ code, message, result }`,不要再叠加响应中间件造成 500 或嵌套响应。 \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2a6e6e9 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +ROOT_DIR = $(shell pwd) +NAMESPACE = "default" +DEPLOY_NAME = "template-single" +DOCKER_NAME = "template-single" + +include ./hack/hack-cli.mk +include ./hack/hack.mk \ No newline at end of file diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..d36cedd --- /dev/null +++ b/README.MD @@ -0,0 +1,4 @@ +# GoFrame Template For SingleRepo + +Quick Start: +- https://goframe.org/quick \ No newline at end of file diff --git a/api/admin/v1/admin.go b/api/admin/v1/admin.go new file mode 100644 index 0000000..07c5886 --- /dev/null +++ b/api/admin/v1/admin.go @@ -0,0 +1,176 @@ +package v1 + +import "github.com/gogf/gf/v2/frame/g" + +// LoginReq 是后台管理员登录请求。 +type LoginReq struct { + g.Meta `path:"/api/admin/login" tags:"后台" method:"post" summary:"后台管理员登录"` + Account string `json:"account" dc:"账号"` + Password string `json:"password" dc:"密码"` +} + +type LoginRes struct{} + +// MenuTreeReq 是后台菜单树请求。 +type MenuTreeReq struct { + g.Meta `path:"/api/admin/menus/tree" tags:"后台菜单" method:"get" summary:"获取后台菜单树"` +} + +type MenuTreeRes struct{} + +// MenuLayoutReq 是后台菜单布局请求。 +type MenuLayoutReq struct { + g.Meta `path:"/api/admin/profile/menu-layout" tags:"后台菜单" method:"get" summary:"获取管理员菜单布局"` +} + +type MenuLayoutRes struct{} + +// UpdateMenuLayoutReq 是后台菜单布局更新请求。 +type UpdateMenuLayoutReq struct { + g.Meta `path:"/api/admin/profile/menu-layout" tags:"后台菜单" method:"post" summary:"更新管理员菜单布局"` + MenuLayout string `json:"menu_layout" dc:"菜单布局 single单列 double双列"` +} + +type UpdateMenuLayoutRes struct{} + +// RolesReq 是后台角色列表请求。 +type RolesReq struct { + g.Meta `path:"/api/admin/roles" tags:"后台角色" method:"get" summary:"获取角色列表"` +} + +type RolesRes struct{} + +// SaveRoleMenusReq 是角色菜单授权保存请求。 +type SaveRoleMenusReq struct { + g.Meta `path:"/api/admin/roles/menu" tags:"后台角色" method:"post" summary:"保存角色菜单授权"` + RoleId int `json:"role_id" dc:"角色ID"` + MenuIds []int `json:"menu_ids" dc:"菜单ID列表"` +} + +type SaveRoleMenusRes struct{} + +// SitePagesReq 获取页面配置列表。 +type SitePagesReq struct { + g.Meta `path:"/api/admin/site/pages" tags:"后台官网" method:"get" summary:"获取官网页面配置"` +} + +type SitePagesRes struct{} + +// SaveSitePageReq 保存页面 SEO/GSO 配置。 +type SaveSitePageReq struct { + g.Meta `path:"/api/admin/site/pages" tags:"后台官网" method:"post" summary:"保存官网页面配置"` + Id int `json:"id" dc:"页面ID"` + Code string `json:"code" dc:"页面编码"` + Title string `json:"title" dc:"页面标题"` + SeoTitle string `json:"seo_title" dc:"SEO标题"` + SeoKeywords string `json:"seo_keywords" dc:"SEO关键词"` + SeoDescription string `json:"seo_description" dc:"SEO描述"` + GsoContent map[string]interface{} `json:"gso_content" dc:"GSO/GEO内容"` + Status int `json:"status" dc:"状态"` + Sort int `json:"sort" dc:"排序"` +} + +type SaveSitePageRes struct{} + +// SiteSectionsReq 获取官网页面模块。 +type SiteSectionsReq struct { + g.Meta `path:"/api/admin/site/sections" tags:"后台官网" method:"get" summary:"获取官网页面模块"` + PageCode string `json:"page_code" dc:"页面编码"` +} + +type SiteSectionsRes struct{} + +// SaveSiteSectionReq 保存官网页面模块。 +type SaveSiteSectionReq struct { + g.Meta `path:"/api/admin/site/sections" tags:"后台官网" method:"post" summary:"保存官网页面模块"` + Id int `json:"id" dc:"模块ID"` + PageCode string `json:"page_code" dc:"页面编码"` + SectionKey string `json:"section_key" dc:"模块标识"` + Title string `json:"title" dc:"模块标题"` + Subtitle string `json:"subtitle" dc:"模块副标题"` + Content map[string]interface{} `json:"content" dc:"模块内容JSON"` + Status int `json:"status" dc:"状态"` + Sort int `json:"sort" dc:"排序"` +} + +type SaveSiteSectionRes struct{} + +// SiteMediaReq 获取官网素材列表。 +type SiteMediaReq struct { + g.Meta `path:"/api/admin/site/media" tags:"后台官网" method:"get" summary:"获取官网素材"` + MediaType string `json:"media_type" dc:"素材类型"` +} + +type SiteMediaRes struct{} + +// SaveSiteMediaReq 保存官网素材。 +type SaveSiteMediaReq struct { + g.Meta `path:"/api/admin/site/media" tags:"后台官网" method:"post" summary:"保存官网素材"` + Id int `json:"id" dc:"素材ID"` + MediaType string `json:"media_type" dc:"素材类型"` + GroupCode string `json:"group_code" dc:"素材分组"` + Title string `json:"title" dc:"素材标题"` + Url string `json:"url" dc:"素材URL"` + Alt string `json:"alt" dc:"替代文本"` + Status int `json:"status" dc:"状态"` + Sort int `json:"sort" dc:"排序"` +} + +type SaveSiteMediaRes struct{} + +// RecruitJobsReq 获取招聘职位列表。 +type RecruitJobsReq struct { + g.Meta `path:"/api/admin/recruit/jobs" tags:"后台招聘" method:"get" summary:"获取招聘职位"` +} + +type RecruitJobsRes struct{} + +// SaveRecruitJobReq 保存招聘职位。 +type SaveRecruitJobReq struct { + g.Meta `path:"/api/admin/recruit/jobs" tags:"后台招聘" method:"post" summary:"保存招聘职位"` + Id int `json:"id" dc:"职位ID"` + Title string `json:"title" dc:"职位名称"` + Department string `json:"department" dc:"部门"` + JobType string `json:"job_type" dc:"职位类型"` + City string `json:"city" dc:"城市"` + Salary string `json:"salary" dc:"薪资"` + Experience string `json:"experience" dc:"经验"` + Description string `json:"description" dc:"职位描述"` + Requirements []string `json:"requirements" dc:"岗位要求"` + Manager string `json:"manager" dc:"负责人"` + ManagerTitle string `json:"manager_title" dc:"负责人职位"` + Email string `json:"email" dc:"邮箱"` + Phone string `json:"phone" dc:"电话"` + Featured bool `json:"featured" dc:"是否推荐"` + Status int `json:"status" dc:"状态"` + Sort int `json:"sort" dc:"排序"` +} + +type SaveRecruitJobRes struct{} + +// SiteSettingsReq 获取站点全局配置。 +type SiteSettingsReq struct { + g.Meta `path:"/api/admin/site/settings" tags:"后台官网" method:"get" summary:"获取站点配置"` +} + +type SiteSettingsRes struct{} + +// SaveSiteSettingsReq 保存站点全局配置。 +type SaveSiteSettingsReq struct { + g.Meta `path:"/api/admin/site/settings" tags:"后台官网" method:"post" summary:"保存站点配置"` + SiteTitle string `json:"site_title" dc:"站点标题"` + SiteLogo string `json:"site_logo" dc:"站点LOGO"` + SiteLogoDark string `json:"site_logo_dark" dc:"暗色LOGO"` + SiteFavicon string `json:"site_favicon" dc:"favicon"` + IcpText string `json:"icp_text" dc:"备案信息"` + Copyright string `json:"copyright" dc:"版权信息"` + ContactPhone string `json:"contact_phone" dc:"联系电话"` + ContactEmail string `json:"contact_email" dc:"联系邮箱"` + ContactAddress string `json:"contact_address" dc:"联系地址"` + DefaultSeo map[string]interface{} `json:"default_seo" dc:"默认SEO"` + DefaultGso map[string]interface{} `json:"default_gso" dc:"默认GSO/GEO"` + AmapKey string `json:"amap_key" dc:"高德Key"` + AmapSecurityCode string `json:"amap_security_code" dc:"高德安全密钥"` +} + +type SaveSiteSettingsRes struct{} diff --git a/api/hello/hello.go b/api/hello/hello.go new file mode 100644 index 0000000..6115abf --- /dev/null +++ b/api/hello/hello.go @@ -0,0 +1,15 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package hello + +import ( + "context" + + "xk-of-api/api/hello/v1" +) + +type IHelloV1 interface { + Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error) +} diff --git a/api/hello/v1/hello.go b/api/hello/v1/hello.go new file mode 100644 index 0000000..b4dd233 --- /dev/null +++ b/api/hello/v1/hello.go @@ -0,0 +1,12 @@ +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" +) + +type HelloReq struct { + g.Meta `path:"/hello" tags:"Hello" method:"get" summary:"You first hello api"` +} +type HelloRes struct { + g.Meta `mime:"text/html" example:"string"` +} diff --git a/api/lead/v1/lead.go b/api/lead/v1/lead.go new file mode 100644 index 0000000..3e21f5f --- /dev/null +++ b/api/lead/v1/lead.go @@ -0,0 +1,24 @@ +package v1 + +import "github.com/gogf/gf/v2/frame/g" + +// CreateReq 是官网咨询线索创建请求。 +type CreateReq struct { + g.Meta `path:"/api/leads" tags:"线索" method:"post" summary:"创建官网咨询线索"` + Name string `json:"name" dc:"联系人姓名"` + Phone string `json:"phone" dc:"联系电话"` + ClinicName string `json:"clinic_name" dc:"诊所名称"` + City string `json:"city" dc:"所在城市"` + PlanCode string `json:"plan_code" dc:"意向套餐编码"` + SourcePage string `json:"source_page" dc:"来源页面"` + Remark string `json:"remark" dc:"备注"` +} + +type CreateRes struct{} + +// AdminListReq 是后台线索列表请求。 +type AdminListReq struct { + g.Meta `path:"/api/admin/leads" tags:"后台线索" method:"get" summary:"获取后台线索列表"` +} + +type AdminListRes struct{} diff --git a/api/order/v1/order.go b/api/order/v1/order.go new file mode 100644 index 0000000..f6899fd --- /dev/null +++ b/api/order/v1/order.go @@ -0,0 +1,30 @@ +package v1 + +import "github.com/gogf/gf/v2/frame/g" + +// CreateReq 是套餐订购订单创建请求。 +type CreateReq struct { + g.Meta `path:"/api/orders" tags:"订单" method:"post" summary:"创建套餐订购订单"` + TenantName string `json:"tenant_name" dc:"租户名称或诊所名称"` + ContactName string `json:"contact_name" dc:"联系人姓名"` + Phone string `json:"phone" dc:"联系电话"` + PlanCode string `json:"plan_code" dc:"套餐编码"` + BillingCycle string `json:"billing_cycle" dc:"订购周期 month月付 year年付"` +} + +type CreateRes struct{} + +// DetailReq 是订单详情请求。 +type DetailReq struct { + g.Meta `path:"/api/orders/{order_no}" tags:"订单" method:"get" summary:"获取订单详情"` + OrderNo string `json:"order_no" in:"path" dc:"订单号"` +} + +type DetailRes struct{} + +// AdminListReq 是后台订单列表请求。 +type AdminListReq struct { + g.Meta `path:"/api/admin/orders" tags:"后台订单" method:"get" summary:"获取后台订单列表"` +} + +type AdminListRes struct{} diff --git a/api/payment/v1/payment.go b/api/payment/v1/payment.go new file mode 100644 index 0000000..a918b90 --- /dev/null +++ b/api/payment/v1/payment.go @@ -0,0 +1,11 @@ +package v1 + +import "github.com/gogf/gf/v2/frame/g" + +// NotifyReq 是支付渠道回调请求。 +type NotifyReq struct { + g.Meta `path:"/api/payments/notify/{channel}" tags:"支付" method:"post" summary:"接收支付渠道回调"` + Channel string `json:"channel" in:"path" dc:"支付渠道"` +} + +type NotifyRes struct{} diff --git a/api/pricing/v1/pricing.go b/api/pricing/v1/pricing.go new file mode 100644 index 0000000..75147e1 --- /dev/null +++ b/api/pricing/v1/pricing.go @@ -0,0 +1,17 @@ +package v1 + +import "github.com/gogf/gf/v2/frame/g" + +// PlansReq 是官网定价套餐请求。 +type PlansReq struct { + g.Meta `path:"/api/pricing/plans" tags:"定价" method:"get" summary:"获取官网定价套餐"` +} + +type PlansRes struct{} + +// AdminPlansReq 是后台套餐列表请求。 +type AdminPlansReq struct { + g.Meta `path:"/api/admin/pricing/plans" tags:"后台定价" method:"get" summary:"获取后台套餐列表"` +} + +type AdminPlansRes struct{} diff --git a/api/site/v1/site.go b/api/site/v1/site.go new file mode 100644 index 0000000..02dd912 --- /dev/null +++ b/api/site/v1/site.go @@ -0,0 +1,24 @@ +package v1 + +import "github.com/gogf/gf/v2/frame/g" + +// HomeReq 是官网首页聚合内容请求。 +type HomeReq struct { + g.Meta `path:"/api/site/home" tags:"官网" method:"get" summary:"获取官网首页内容"` +} + +type HomeRes struct{} + +// SettingsReq 获取前台站点公开配置。 +type SettingsReq struct { + g.Meta `path:"/api/site/settings" tags:"官网" method:"get" summary:"获取站点公开配置"` +} + +type SettingsRes struct{} + +// RecruitJobsReq 获取前台招聘职位。 +type RecruitJobsReq struct { + g.Meta `path:"/api/recruit/jobs" tags:"招聘" method:"get" summary:"获取招聘职位"` +} + +type RecruitJobsRes struct{} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f150e45 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module xk-of-api + +go 1.23.0 + +require github.com/gogf/gf/v2 v2.10.0 + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grokify/html-strip-tags-go v0.1.0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/olekukonko/tablewriter v1.1.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.25.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..17d75c3 --- /dev/null +++ b/go.sum @@ -0,0 +1,81 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= +github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs= +github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= +github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/config.yaml b/hack/config.yaml new file mode 100644 index 0000000..83c7dc6 --- /dev/null +++ b/hack/config.yaml @@ -0,0 +1,13 @@ + +# CLI tool, only in development environment. +# https://goframe.org/docs/cli +gfcli: + gen: + dao: + - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + descriptionTag: true + + docker: + build: "-a amd64 -s linux -p temp -ew" + tagPrefixes: + - my.image.pub/my-app \ No newline at end of file diff --git a/hack/hack-cli.mk b/hack/hack-cli.mk new file mode 100644 index 0000000..526124e --- /dev/null +++ b/hack/hack-cli.mk @@ -0,0 +1,18 @@ + +# Install/Update to the latest CLI tool. +.PHONY: cli +cli: + @set -e; \ + echo "go install github.com/gogf/gf/cmd/gf/v2@latest"; \ + go install github.com/gogf/gf/cmd/gf/v2@latest; \ + echo "GoFame CLI installed successfully!" + + +# Check and install CLI tool. +.PHONY: cli.install +cli.install: + @set -e; \ + gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \ + echo "GoFame CLI is not installed, start proceeding auto installation..."; \ + make cli; \ + fi; \ No newline at end of file diff --git a/hack/hack.mk b/hack/hack.mk new file mode 100644 index 0000000..2f68179 --- /dev/null +++ b/hack/hack.mk @@ -0,0 +1,75 @@ +.DEFAULT_GOAL := build + +# Update GoFrame and its CLI to latest stable version. +.PHONY: up +up: cli.install + @gf up -a + +# Build binary using configuration from hack/config.yaml. +.PHONY: build +build: cli.install + @gf build -ew + +# Parse api and generate controller/sdk. +.PHONY: ctrl +ctrl: cli.install + @gf gen ctrl + +# Generate Go files for DAO/DO/Entity. +.PHONY: dao +dao: cli.install + @gf gen dao + +# Parse current project go files and generate enums go file. +.PHONY: enums +enums: cli.install + @gf gen enums + +# Generate Go files for Service. +.PHONY: service +service: cli.install + @gf gen service + + +# Build docker image. +.PHONY: image +image: cli.install + $(eval _TAG = $(shell git rev-parse --short HEAD)) +ifneq (, $(shell git status --porcelain 2>/dev/null)) + $(eval _TAG = $(_TAG).dirty) +endif + $(eval _TAG = $(if ${TAG}, ${TAG}, $(_TAG))) + $(eval _PUSH = $(if ${PUSH}, ${PUSH}, )) + @gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG}; + + +# Build docker image and automatically push to docker repo. +.PHONY: image.push +image.push: cli.install + @make image PUSH=-p; + + +# Deploy image and yaml to current kubectl environment. +.PHONY: deploy +deploy: cli.install + $(eval _TAG = $(if ${TAG}, ${TAG}, develop)) + + @set -e; \ + mkdir -p $(ROOT_DIR)/temp/kustomize;\ + cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_ENV};\ + kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\ + kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \ + if [ $(DEPLOY_NAME) != "" ]; then \ + kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}"; \ + fi; + + +# Parsing protobuf files and generating go files. +.PHONY: pb +pb: cli.install + @gf gen pb + +# Generate protobuf files for database tables. +.PHONY: pbentity +pbentity: cli.install + @gf gen pbentity \ No newline at end of file diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go new file mode 100644 index 0000000..2a63faf --- /dev/null +++ b/internal/cmd/cmd.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gcmd" + + "xk-of-api/internal/controller/admin" + "xk-of-api/internal/controller/hello" + "xk-of-api/internal/controller/lead" + "xk-of-api/internal/controller/order" + "xk-of-api/internal/controller/payment" + "xk-of-api/internal/controller/pricing" + "xk-of-api/internal/controller/site" +) + +var ( + // Main 是 HTTP 服务启动命令,集中完成中间件和控制器绑定。 + Main = gcmd.Command{ + Name: "main", + Usage: "main", + Brief: "start http server", + Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + s := g.Server() + s.Group("/", func(group *ghttp.RouterGroup) { + group.Middleware(ghttp.MiddlewareCORS) + group.Bind( + hello.NewV1(), + site.NewV1(), + pricing.NewV1(), + lead.NewV1(), + order.NewV1(), + payment.NewV1(), + admin.NewV1(), + ) + }) + s.Run() + return nil + }, + } +) diff --git a/internal/consts/consts.go b/internal/consts/consts.go new file mode 100644 index 0000000..d709a2b --- /dev/null +++ b/internal/consts/consts.go @@ -0,0 +1 @@ +package consts diff --git a/internal/controller/admin/admin.go b/internal/controller/admin/admin.go new file mode 100644 index 0000000..fd6ee6d --- /dev/null +++ b/internal/controller/admin/admin.go @@ -0,0 +1,9 @@ +package admin + +// ControllerV1 是后台管理控制器。 +type ControllerV1 struct{} + +// NewV1 创建后台管理控制器实例。 +func NewV1() *ControllerV1 { + return &ControllerV1{} +} diff --git a/internal/controller/admin/admin_v1.go b/internal/controller/admin/admin_v1.go new file mode 100644 index 0000000..cb8b49e --- /dev/null +++ b/internal/controller/admin/admin_v1.go @@ -0,0 +1,111 @@ +package admin + +import ( + "context" + + "xk-of-api/api/admin/v1" + "xk-of-api/internal/model" + "xk-of-api/internal/response" + "xk-of-api/internal/service" +) + +// Login 处理后台登录请求。 +func (c *ControllerV1) Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) { + response.Success(ctx, service.Admin.Login(req.Account)) + return nil, nil +} + +// MenuTree 返回后台多级菜单树。 +func (c *ControllerV1) MenuTree(ctx context.Context, req *v1.MenuTreeReq) (res *v1.MenuTreeRes, err error) { + response.Success(ctx, service.Admin.Menus()) + return nil, nil +} + +// MenuLayout 返回当前管理员菜单布局。 +func (c *ControllerV1) MenuLayout(ctx context.Context, req *v1.MenuLayoutReq) (res *v1.MenuLayoutRes, err error) { + response.Success(ctx, service.Admin.MenuLayout()) + return nil, nil +} + +// UpdateMenuLayout 更新当前管理员菜单布局偏好。 +func (c *ControllerV1) UpdateMenuLayout(ctx context.Context, req *v1.UpdateMenuLayoutReq) (res *v1.UpdateMenuLayoutRes, err error) { + response.Success(ctx, service.Admin.UpdateMenuLayout(req.MenuLayout)) + return nil, nil +} + +// Roles 返回角色列表。 +func (c *ControllerV1) Roles(ctx context.Context, req *v1.RolesReq) (res *v1.RolesRes, err error) { + response.Success(ctx, service.Admin.Roles()) + return nil, nil +} + +// SaveRoleMenus 保存角色菜单授权。 +func (c *ControllerV1) SaveRoleMenus(ctx context.Context, req *v1.SaveRoleMenusReq) (res *v1.SaveRoleMenusRes, err error) { + response.Success(ctx, service.Admin.SaveRoleMenus(req.RoleId, req.MenuIds)) + return nil, nil +} + +// SitePages 获取页面 SEO/GSO 配置。 +func (c *ControllerV1) SitePages(ctx context.Context, req *v1.SitePagesReq) (res *v1.SitePagesRes, err error) { + response.Success(ctx, service.Admin.SitePages()) + return nil, nil +} + +// SaveSitePage 保存页面 SEO/GSO 配置。 +func (c *ControllerV1) SaveSitePage(ctx context.Context, req *v1.SaveSitePageReq) (res *v1.SaveSitePageRes, err error) { + page := model.SitePage{Id: req.Id, Code: req.Code, Title: req.Title, SeoTitle: req.SeoTitle, SeoKeywords: req.SeoKeywords, SeoDescription: req.SeoDescription, GsoContent: req.GsoContent, Status: req.Status, Sort: req.Sort} + response.Success(ctx, service.Admin.SaveSitePage(page)) + return nil, nil +} + +// SiteSections 获取官网页面模块。 +func (c *ControllerV1) SiteSections(ctx context.Context, req *v1.SiteSectionsReq) (res *v1.SiteSectionsRes, err error) { + response.Success(ctx, service.Admin.SiteSections(req.PageCode)) + return nil, nil +} + +// SaveSiteSection 保存官网页面模块。 +func (c *ControllerV1) SaveSiteSection(ctx context.Context, req *v1.SaveSiteSectionReq) (res *v1.SaveSiteSectionRes, err error) { + section := model.SiteSection{Id: req.Id, PageCode: req.PageCode, SectionKey: req.SectionKey, Title: req.Title, Subtitle: req.Subtitle, Content: req.Content, Status: req.Status, Sort: req.Sort} + response.Success(ctx, service.Admin.SaveSiteSection(section)) + return nil, nil +} + +// SiteMedia 获取官网素材列表。 +func (c *ControllerV1) SiteMedia(ctx context.Context, req *v1.SiteMediaReq) (res *v1.SiteMediaRes, err error) { + response.Success(ctx, service.Admin.SiteMedia(req.MediaType)) + return nil, nil +} + +// SaveSiteMedia 保存官网素材。 +func (c *ControllerV1) SaveSiteMedia(ctx context.Context, req *v1.SaveSiteMediaReq) (res *v1.SaveSiteMediaRes, err error) { + media := model.SiteMedia{Id: req.Id, MediaType: req.MediaType, GroupCode: req.GroupCode, Title: req.Title, Url: req.Url, Alt: req.Alt, Status: req.Status, Sort: req.Sort} + response.Success(ctx, service.Admin.SaveSiteMedia(media)) + return nil, nil +} + +// RecruitJobs 获取后台招聘职位列表。 +func (c *ControllerV1) RecruitJobs(ctx context.Context, req *v1.RecruitJobsReq) (res *v1.RecruitJobsRes, err error) { + response.Success(ctx, service.Admin.RecruitJobs()) + return nil, nil +} + +// SaveRecruitJob 保存招聘职位。 +func (c *ControllerV1) SaveRecruitJob(ctx context.Context, req *v1.SaveRecruitJobReq) (res *v1.SaveRecruitJobRes, err error) { + job := model.RecruitJob{Id: req.Id, Title: req.Title, Department: req.Department, JobType: req.JobType, City: req.City, Salary: req.Salary, Experience: req.Experience, Description: req.Description, Requirements: req.Requirements, Manager: req.Manager, ManagerTitle: req.ManagerTitle, Email: req.Email, Phone: req.Phone, Featured: req.Featured, Status: req.Status, Sort: req.Sort} + response.Success(ctx, service.Admin.SaveRecruitJob(job)) + return nil, nil +} + +// SiteSettings 获取站点全局配置。 +func (c *ControllerV1) SiteSettings(ctx context.Context, req *v1.SiteSettingsReq) (res *v1.SiteSettingsRes, err error) { + response.Success(ctx, service.Admin.SiteSettings()) + return nil, nil +} + +// SaveSiteSettings 保存站点全局配置。 +func (c *ControllerV1) SaveSiteSettings(ctx context.Context, req *v1.SaveSiteSettingsReq) (res *v1.SaveSiteSettingsRes, err error) { + settings := model.SiteSettings{SiteTitle: req.SiteTitle, SiteLogo: req.SiteLogo, SiteLogoDark: req.SiteLogoDark, SiteFavicon: req.SiteFavicon, IcpText: req.IcpText, Copyright: req.Copyright, ContactPhone: req.ContactPhone, ContactEmail: req.ContactEmail, ContactAddress: req.ContactAddress, DefaultSeo: req.DefaultSeo, DefaultGso: req.DefaultGso, AmapKey: req.AmapKey, AmapSecurityCode: req.AmapSecurityCode} + response.Success(ctx, service.Admin.SaveSiteSettings(settings)) + return nil, nil +} diff --git a/internal/controller/hello/hello.go b/internal/controller/hello/hello.go new file mode 100644 index 0000000..f72082f --- /dev/null +++ b/internal/controller/hello/hello.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package hello diff --git a/internal/controller/hello/hello_new.go b/internal/controller/hello/hello_new.go new file mode 100644 index 0000000..bdc1982 --- /dev/null +++ b/internal/controller/hello/hello_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package hello + +import ( + "xk-of-api/api/hello" +) + +type ControllerV1 struct{} + +func NewV1() hello.IHelloV1 { + return &ControllerV1{} +} diff --git a/internal/controller/hello/hello_v1_hello.go b/internal/controller/hello/hello_v1_hello.go new file mode 100644 index 0000000..773807c --- /dev/null +++ b/internal/controller/hello/hello_v1_hello.go @@ -0,0 +1,13 @@ +package hello + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" + + "xk-of-api/api/hello/v1" +) + +func (c *ControllerV1) Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error) { + g.RequestFromCtx(ctx).Response.Writeln("Hello World!") + return +} diff --git a/internal/controller/lead/lead.go b/internal/controller/lead/lead.go new file mode 100644 index 0000000..482d468 --- /dev/null +++ b/internal/controller/lead/lead.go @@ -0,0 +1,9 @@ +package lead + +// ControllerV1 是官网线索控制器。 +type ControllerV1 struct{} + +// NewV1 创建官网线索控制器实例。 +func NewV1() *ControllerV1 { + return &ControllerV1{} +} diff --git a/internal/controller/lead/lead_v1.go b/internal/controller/lead/lead_v1.go new file mode 100644 index 0000000..9752838 --- /dev/null +++ b/internal/controller/lead/lead_v1.go @@ -0,0 +1,27 @@ +package lead + +import ( + "context" + + "xk-of-api/api/lead/v1" + "xk-of-api/internal/model" + "xk-of-api/internal/response" + "xk-of-api/internal/service" +) + +// Create 创建官网咨询或预约演示线索。 +func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) { + result, err := service.Lead.Create(model.Lead{Name: req.Name, Phone: req.Phone, ClinicName: req.ClinicName, City: req.City, PlanCode: req.PlanCode, SourcePage: req.SourcePage, Remark: req.Remark}) + if err != nil { + response.Error(ctx, 400, err.Error()) + return nil, nil + } + response.Success(ctx, result) + return nil, nil +} + +// AdminList 获取后台线索列表。 +func (c *ControllerV1) AdminList(ctx context.Context, req *v1.AdminListReq) (res *v1.AdminListRes, err error) { + response.Success(ctx, service.Lead.List()) + return nil, nil +} diff --git a/internal/controller/order/order.go b/internal/controller/order/order.go new file mode 100644 index 0000000..2bf739e --- /dev/null +++ b/internal/controller/order/order.go @@ -0,0 +1,9 @@ +package order + +// ControllerV1 是订购订单控制器。 +type ControllerV1 struct{} + +// NewV1 创建订购订单控制器实例。 +func NewV1() *ControllerV1 { + return &ControllerV1{} +} diff --git a/internal/controller/order/order_v1.go b/internal/controller/order/order_v1.go new file mode 100644 index 0000000..00fb1cf --- /dev/null +++ b/internal/controller/order/order_v1.go @@ -0,0 +1,37 @@ +package order + +import ( + "context" + + "xk-of-api/api/order/v1" + "xk-of-api/internal/model" + "xk-of-api/internal/response" + "xk-of-api/internal/service" +) + +// Create 创建套餐订购订单。 +func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) { + result, err := service.Order.Create(model.Order{TenantName: req.TenantName, ContactName: req.ContactName, Phone: req.Phone, PlanCode: req.PlanCode, BillingCycle: req.BillingCycle}) + if err != nil { + response.Error(ctx, 400, err.Error()) + return nil, nil + } + response.Success(ctx, result) + return nil, nil +} + +// Detail 获取订单详情。 +func (c *ControllerV1) Detail(ctx context.Context, req *v1.DetailReq) (res *v1.DetailRes, err error) { + if req.OrderNo == "" { + response.Error(ctx, 400, "订单号不能为空") + return nil, nil + } + response.Success(ctx, service.Order.Detail(req.OrderNo)) + return nil, nil +} + +// AdminList 获取后台订单列表。 +func (c *ControllerV1) AdminList(ctx context.Context, req *v1.AdminListReq) (res *v1.AdminListRes, err error) { + response.Success(ctx, service.Order.List()) + return nil, nil +} diff --git a/internal/controller/payment/payment.go b/internal/controller/payment/payment.go new file mode 100644 index 0000000..4de14fb --- /dev/null +++ b/internal/controller/payment/payment.go @@ -0,0 +1,9 @@ +package payment + +// ControllerV1 是支付回调控制器。 +type ControllerV1 struct{} + +// NewV1 创建支付回调控制器实例。 +func NewV1() *ControllerV1 { + return &ControllerV1{} +} diff --git a/internal/controller/payment/payment_v1.go b/internal/controller/payment/payment_v1.go new file mode 100644 index 0000000..9b9f8f5 --- /dev/null +++ b/internal/controller/payment/payment_v1.go @@ -0,0 +1,21 @@ +package payment + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" + + "xk-of-api/api/payment/v1" + "xk-of-api/internal/response" + "xk-of-api/internal/service" +) + +// Notify 接收支付渠道回调,后续在 service 中扩展验签和状态流转。 +func (c *ControllerV1) Notify(ctx context.Context, req *v1.NotifyReq) (res *v1.NotifyRes, err error) { + if req.Channel == "" { + response.Error(ctx, 400, "支付渠道不能为空") + return nil, nil + } + response.Success(ctx, service.Order.Notify(req.Channel, g.RequestFromCtx(ctx).GetMap())) + return nil, nil +} diff --git a/internal/controller/pricing/pricing.go b/internal/controller/pricing/pricing.go new file mode 100644 index 0000000..45abf65 --- /dev/null +++ b/internal/controller/pricing/pricing.go @@ -0,0 +1,9 @@ +package pricing + +// ControllerV1 是套餐定价控制器。 +type ControllerV1 struct{} + +// NewV1 创建套餐定价控制器实例。 +func NewV1() *ControllerV1 { + return &ControllerV1{} +} diff --git a/internal/controller/pricing/pricing_v1.go b/internal/controller/pricing/pricing_v1.go new file mode 100644 index 0000000..604f7d1 --- /dev/null +++ b/internal/controller/pricing/pricing_v1.go @@ -0,0 +1,21 @@ +package pricing + +import ( + "context" + + "xk-of-api/api/pricing/v1" + "xk-of-api/internal/response" + "xk-of-api/internal/service" +) + +// Plans 获取官网可展示套餐。 +func (c *ControllerV1) Plans(ctx context.Context, req *v1.PlansReq) (res *v1.PlansRes, err error) { + response.Success(ctx, service.Pricing.Plans()) + return nil, nil +} + +// AdminPlans 获取后台套餐列表。 +func (c *ControllerV1) AdminPlans(ctx context.Context, req *v1.AdminPlansReq) (res *v1.AdminPlansRes, err error) { + response.Success(ctx, service.Pricing.Plans()) + return nil, nil +} diff --git a/internal/controller/site/site.go b/internal/controller/site/site.go new file mode 100644 index 0000000..3f4230a --- /dev/null +++ b/internal/controller/site/site.go @@ -0,0 +1,9 @@ +package site + +// ControllerV1 是官网内容控制器。 +type ControllerV1 struct{} + +// NewV1 创建官网内容控制器实例。 +func NewV1() *ControllerV1 { + return &ControllerV1{} +} diff --git a/internal/controller/site/site_v1_home.go b/internal/controller/site/site_v1_home.go new file mode 100644 index 0000000..7897b6a --- /dev/null +++ b/internal/controller/site/site_v1_home.go @@ -0,0 +1,27 @@ +package site + +import ( + "context" + + "xk-of-api/api/site/v1" + "xk-of-api/internal/response" + "xk-of-api/internal/service" +) + +// Home 获取官网首页聚合内容。 +func (c *ControllerV1) Home(ctx context.Context, req *v1.HomeReq) (res *v1.HomeRes, err error) { + response.Success(ctx, service.Site.Home()) + return nil, nil +} + +// Settings 获取前台站点公开配置。 +func (c *ControllerV1) Settings(ctx context.Context, req *v1.SettingsReq) (res *v1.SettingsRes, err error) { + response.Success(ctx, service.Site.Settings()) + return nil, nil +} + +// RecruitJobs 获取前台招聘职位。 +func (c *ControllerV1) RecruitJobs(ctx context.Context, req *v1.RecruitJobsReq) (res *v1.RecruitJobsRes, err error) { + response.Success(ctx, service.Site.RecruitJobs()) + return nil, nil +} diff --git a/internal/dao/.gitkeep b/internal/dao/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/model/.gitkeep b/internal/model/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/model/do/.gitkeep b/internal/model/do/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/model/entity/.gitkeep b/internal/model/entity/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/model/website.go b/internal/model/website.go new file mode 100644 index 0000000..34eb8cd --- /dev/null +++ b/internal/model/website.go @@ -0,0 +1,183 @@ +package model + +// SiteHome 是官网首页聚合数据,供前端一次性渲染核心官网模块。 +type SiteHome struct { + Hero HeroBlock `json:"hero"` + Products []ProductBlock `json:"products"` + Highlights []ValueBlock `json:"highlights"` + Faqs []FaqBlock `json:"faqs"` +} + +// HeroBlock 描述官网主视觉文案和关键指标。 +type HeroBlock struct { + Title string `json:"title"` + Subtitle string `json:"subtitle"` + PrimaryCTA string `json:"primary_cta"` + SecondaryCTA string `json:"secondary_cta"` + Stats []StatBlock `json:"stats"` +} + +// StatBlock 是官网展示的关键业务指标。 +type StatBlock struct { + Value string `json:"value"` + Label string `json:"label"` +} + +// ProductBlock 描述患者端、医生端、老板端、推广员端和 PC 后台。 +type ProductBlock struct { + Name string `json:"name"` + Role string `json:"role"` + Description string `json:"description"` + Features []string `json:"features"` +} + +// ValueBlock 描述诊所 SaaS 的核心能力。 +type ValueBlock struct { + Title string `json:"title"` + Description string `json:"description"` + Icon string `json:"icon"` +} + +// FaqBlock 描述官网常见问题。 +type FaqBlock struct { + Question string `json:"question"` + Answer string `json:"answer"` +} + +// Plan 是 SaaS 套餐展示与订购使用的数据模型。 +type Plan struct { + Id int `json:"id"` + Code string `json:"code"` + Name string `json:"name"` + Description string `json:"description"` + MonthlyFee int `json:"monthly_fee"` + YearlyFee int `json:"yearly_fee"` + Recommended bool `json:"recommended"` + Features []PlanFeature `json:"features"` +} + +// PlanFeature 是套餐权益明细。 +type PlanFeature struct { + Name string `json:"name"` + Value string `json:"value"` + Highlight bool `json:"highlight"` +} + +// Lead 是官网咨询或预约演示线索。 +type Lead struct { + Name string `json:"name"` + Phone string `json:"phone"` + ClinicName string `json:"clinic_name"` + City string `json:"city"` + PlanCode string `json:"plan_code"` + SourcePage string `json:"source_page"` + Remark string `json:"remark"` +} + +// Order 是套餐订购订单。 +type Order struct { + OrderNo string `json:"order_no"` + TenantName string `json:"tenant_name"` + ContactName string `json:"contact_name"` + Phone string `json:"phone"` + PlanCode string `json:"plan_code"` + BillingCycle string `json:"billing_cycle"` + Amount int `json:"amount"` + Status string `json:"status"` +} + +// Menu 是后台多级菜单节点,支持单列和双列布局渲染。 +type Menu struct { + Id int `json:"id"` + ParentId int `json:"parent_id"` + Title string `json:"title"` + Path string `json:"path"` + Component string `json:"component"` + Icon string `json:"icon"` + Type string `json:"type"` + Permission string `json:"permission"` + MenuArea int `json:"menu_area"` + Children []Menu `json:"children"` +} + +// MenuLayout 是后台菜单布局配置。 +type MenuLayout struct { + GlobalLayout string `json:"global_layout"` + UserLayout string `json:"user_layout"` + FinalLayout string `json:"final_layout"` +} + +// SitePage 是官网页面配置,包含 SEO 与 GSO/GEO 信息。 +type SitePage struct { + Id int `json:"id"` + Code string `json:"code"` + Title string `json:"title"` + SeoTitle string `json:"seo_title"` + SeoKeywords string `json:"seo_keywords"` + SeoDescription string `json:"seo_description"` + GsoContent map[string]interface{} `json:"gso_content"` + Status int `json:"status"` + Sort int `json:"sort"` +} + +// SiteSection 是官网页面模块配置,内容 JSON 承载各模块可编辑字段。 +type SiteSection struct { + Id int `json:"id"` + PageCode string `json:"page_code"` + SectionKey string `json:"section_key"` + Title string `json:"title"` + Subtitle string `json:"subtitle"` + Content map[string]interface{} `json:"content"` + Status int `json:"status"` + Sort int `json:"sort"` +} + +// SiteMedia 是官网素材,覆盖 LOGO、favicon、页面图、分享图和招聘图。 +type SiteMedia struct { + Id int `json:"id"` + MediaType string `json:"media_type"` + GroupCode string `json:"group_code"` + Title string `json:"title"` + Url string `json:"url"` + Alt string `json:"alt"` + Status int `json:"status"` + Sort int `json:"sort"` +} + +// RecruitJob 是招聘职位信息。 +type RecruitJob struct { + Id int `json:"id"` + Title string `json:"title"` + Department string `json:"department"` + JobType string `json:"job_type"` + City string `json:"city"` + Salary string `json:"salary"` + Experience string `json:"experience"` + Description string `json:"description"` + Requirements []string `json:"requirements"` + Manager string `json:"manager"` + ManagerTitle string `json:"manager_title"` + Email string `json:"email"` + Phone string `json:"phone"` + Featured bool `json:"featured"` + Status int `json:"status"` + Sort int `json:"sort"` + PublishedDate string `json:"published_date"` +} + +// SiteSettings 是站点全局配置,页面未配置时作为默认值。 +type SiteSettings struct { + SiteTitle string `json:"site_title"` + SiteLogo string `json:"site_logo"` + SiteLogoDark string `json:"site_logo_dark"` + SiteFavicon string `json:"site_favicon"` + IcpText string `json:"icp_text"` + Copyright string `json:"copyright"` + ContactPhone string `json:"contact_phone"` + ContactEmail string `json:"contact_email"` + ContactAddress string `json:"contact_address"` + DefaultSeo map[string]interface{} `json:"default_seo"` + DefaultGso map[string]interface{} `json:"default_gso"` + AmapKey string `json:"amap_key"` + AmapSecurityCode string `json:"amap_security_code"` +} diff --git a/internal/response/response.go b/internal/response/response.go new file mode 100644 index 0000000..618402c --- /dev/null +++ b/internal/response/response.go @@ -0,0 +1,38 @@ +package response + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" +) + +// Payload 是所有接口统一返回结构。 +// HTTP 状态码统一保持 200,前端通过 code 判断业务成功或失败。 +type Payload struct { + Code int `json:"code"` + Message string `json:"message"` + Result interface{} `json:"result"` +} + +// write 写出统一 JSON 并立即结束请求,避免 GF 默认响应中间件二次包装。 +func write(ctx context.Context, payload Payload) { + r := g.RequestFromCtx(ctx) + r.Response.Status = 200 + r.Response.WriteJsonExit(payload) +} + +// Success 返回统一成功响应,result 字段承载业务数据。 +func Success(ctx context.Context, result interface{}) { + write(ctx, Payload{Code: 0, Message: "ok", Result: result}) +} + +// Error 返回统一失败响应,HTTP 状态仍为 200,code 用于表达业务错误。 +func Error(ctx context.Context, code int, message string) { + if code == 0 { + code = 500 + } + if message == "" { + message = "服务器开小差了,请稍后重试" + } + write(ctx, Payload{Code: code, Message: message, Result: nil}) +} diff --git a/internal/service/.gitkeep b/internal/service/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/service/admin.go b/internal/service/admin.go new file mode 100644 index 0000000..f1166ae --- /dev/null +++ b/internal/service/admin.go @@ -0,0 +1,386 @@ +package service + +import ( + "sort" + "sync" + + "xk-of-api/internal/model" +) + +// AdminService 负责后台登录、菜单、权限和官网 CMS 管理等后台业务逻辑。 +type AdminService struct{} + +// Admin 暴露后台服务单例,控制器只调用服务层,不直接处理业务细节。 +var Admin = AdminService{} + +var adminStore = struct { + sync.RWMutex + settings model.SiteSettings + pages []model.SitePage + sections []model.SiteSection + media []model.SiteMedia + jobs []model.RecruitJob +}{ + settings: defaultSiteSettings(), + pages: defaultSitePages(), + sections: defaultSiteSections(), + media: defaultSiteMedia(), + jobs: defaultRecruitJobs(), +} + +// Login 返回后台登录占位结果,后续接入 nl_admin 密码校验和 token 签发。 +func (s AdminService) Login(account string) map[string]interface{} { + if account == "" { + account = "admin" + } + return map[string]interface{}{"token": "dev-token", "user": map[string]interface{}{"name": account, "role": "super_admin"}} +} + +// Menus 返回支持单列和双列布局的后台多级菜单树。 +func (s AdminService) Menus() []model.Menu { + return []model.Menu{ + {Id: 1, ParentId: 0, Title: "控制台", Path: "/dashboard", Component: "Dashboard", Icon: "dashboard", Type: "menu", Permission: "dashboard:view", MenuArea: 1}, + {Id: 2, ParentId: 0, Title: "官网运营", Path: "/site", Component: "Layout", Icon: "global", Type: "catalog", Permission: "site:view", MenuArea: 1, Children: []model.Menu{ + {Id: 21, ParentId: 2, Title: "页面模块", Path: "/site/sections", Component: "SiteSections", Icon: "blocks", Type: "menu", Permission: "site:section:view", MenuArea: 2}, + {Id: 22, ParentId: 2, Title: "咨询线索", Path: "/site/leads", Component: "SiteLeads", Icon: "message", Type: "menu", Permission: "site:lead:view", MenuArea: 2}, + {Id: 23, ParentId: 2, Title: "图片管理", Path: "/site/media", Component: "SiteMedia", Icon: "image", Type: "menu", Permission: "site:media:view", MenuArea: 2}, + {Id: 24, ParentId: 2, Title: "招聘管理", Path: "/site/jobs", Component: "RecruitJobs", Icon: "briefcase", Type: "menu", Permission: "site:job:view", MenuArea: 2}, + {Id: 25, ParentId: 2, Title: "站点设置", Path: "/site/settings", Component: "SiteSettings", Icon: "setting", Type: "menu", Permission: "site:settings:view", MenuArea: 2}, + }}, + {Id: 3, ParentId: 0, Title: "商业化", Path: "/commerce", Component: "Layout", Icon: "wallet", Type: "catalog", Permission: "commerce:view", MenuArea: 1, Children: []model.Menu{ + {Id: 31, ParentId: 3, Title: "套餐管理", Path: "/commerce/plans", Component: "PlanList", Icon: "package", Type: "menu", Permission: "plan:view", MenuArea: 2}, + {Id: 32, ParentId: 3, Title: "订单管理", Path: "/commerce/orders", Component: "OrderList", Icon: "orders", Type: "menu", Permission: "order:view", MenuArea: 2}, + }}, + {Id: 4, ParentId: 0, Title: "系统管理", Path: "/system", Component: "Layout", Icon: "setting", Type: "catalog", Permission: "system:view", MenuArea: 1, Children: []model.Menu{ + {Id: 41, ParentId: 4, Title: "管理员", Path: "/system/admins", Component: "AdminList", Icon: "user", Type: "menu", Permission: "admin:view", MenuArea: 2}, + {Id: 42, ParentId: 4, Title: "角色权限", Path: "/system/roles", Component: "RoleList", Icon: "safety", Type: "menu", Permission: "role:view", MenuArea: 2}, + {Id: 43, ParentId: 4, Title: "菜单管理", Path: "/system/menus", Component: "MenuList", Icon: "menu", Type: "menu", Permission: "menu:view", MenuArea: 2}, + }}, + } +} + +// MenuLayout 返回全局默认布局和用户最终布局。 +func (s AdminService) MenuLayout() model.MenuLayout { + return model.MenuLayout{GlobalLayout: "double", UserLayout: "", FinalLayout: "double"} +} + +// UpdateMenuLayout 保存管理员个人菜单布局偏好。 +func (s AdminService) UpdateMenuLayout(layout string) model.MenuLayout { + if layout != "single" && layout != "double" { + layout = "double" + } + return model.MenuLayout{GlobalLayout: "double", UserLayout: layout, FinalLayout: layout} +} + +// Roles 返回后台角色占位数据。 +func (s AdminService) Roles() []map[string]interface{} { + return []map[string]interface{}{{"id": 1, "name": "超级管理员", "code": "super_admin"}, {"id": 2, "name": "运营人员", "code": "operator"}} +} + +// SaveRoleMenus 保存角色菜单授权占位结果。 +func (s AdminService) SaveRoleMenus(roleId int, menuIds []int) map[string]interface{} { + return map[string]interface{}{"role_id": roleId, "menu_ids": menuIds, "saved": true} +} + +// SitePages 返回官网页面配置,支持每个页面独立维护 SEO 和 GSO/GEO 内容。 +func (s AdminService) SitePages() []model.SitePage { + adminStore.RLock() + defer adminStore.RUnlock() + return copyPages(adminStore.pages) +} + +// SaveSitePage 保存页面 SEO/GSO 配置;一期先写入内存,后续替换为 nl_site_page 持久化。 +func (s AdminService) SaveSitePage(page model.SitePage) model.SitePage { + adminStore.Lock() + defer adminStore.Unlock() + if page.Id == 0 { + page.Id = nextPageId(adminStore.pages) + } + if page.GsoContent == nil { + page.GsoContent = map[string]interface{}{} + } + adminStore.pages = upsertPage(adminStore.pages, page) + return page +} + +// SiteSections 返回官网页面模块配置,可按页面编码过滤。 +func (s AdminService) SiteSections(pageCode string) []model.SiteSection { + adminStore.RLock() + defer adminStore.RUnlock() + sections := make([]model.SiteSection, 0) + for _, section := range adminStore.sections { + if pageCode == "" || section.PageCode == pageCode { + sections = append(sections, section) + } + } + sortSections(sections) + return sections +} + +// SaveSiteSection 保存官网页面模块;内容字段保持 JSON 结构,便于后台控制标题、按钮、列表和图片。 +func (s AdminService) SaveSiteSection(section model.SiteSection) model.SiteSection { + adminStore.Lock() + defer adminStore.Unlock() + if section.Id == 0 { + section.Id = nextSectionId(adminStore.sections) + } + if section.Content == nil { + section.Content = map[string]interface{}{} + } + adminStore.sections = upsertSection(adminStore.sections, section) + return section +} + +// SiteMedia 返回官网素材,覆盖 LOGO、favicon、页面图片、分享图和招聘图。 +func (s AdminService) SiteMedia(mediaType string) []model.SiteMedia { + adminStore.RLock() + defer adminStore.RUnlock() + items := make([]model.SiteMedia, 0) + for _, item := range adminStore.media { + if mediaType == "" || item.MediaType == mediaType { + items = append(items, item) + } + } + sortMedia(items) + return items +} + +// SaveSiteMedia 保存官网素材 URL;真实上传和对象存储可在后续扩展。 +func (s AdminService) SaveSiteMedia(media model.SiteMedia) model.SiteMedia { + adminStore.Lock() + defer adminStore.Unlock() + if media.Id == 0 { + media.Id = nextMediaId(adminStore.media) + } + adminStore.media = upsertMedia(adminStore.media, media) + return media +} + +// RecruitJobs 返回后台招聘职位列表。 +func (s AdminService) RecruitJobs() []model.RecruitJob { + adminStore.RLock() + defer adminStore.RUnlock() + jobs := append([]model.RecruitJob(nil), adminStore.jobs...) + sortJobs(jobs) + return jobs +} + +// SaveRecruitJob 保存招聘职位;一期先写入内存,后续接入 nl_recruit_job。 +func (s AdminService) SaveRecruitJob(job model.RecruitJob) model.RecruitJob { + adminStore.Lock() + defer adminStore.Unlock() + if job.Id == 0 { + job.Id = nextJobId(adminStore.jobs) + } + if job.Requirements == nil { + job.Requirements = []string{} + } + if job.PublishedDate == "" { + job.PublishedDate = "2026-07-05" + } + adminStore.jobs = upsertJob(adminStore.jobs, job) + return job +} + +// SiteSettings 返回站点全局配置,页面未配置时使用这里的默认值。 +func (s AdminService) SiteSettings() model.SiteSettings { + adminStore.RLock() + defer adminStore.RUnlock() + return copySettings(adminStore.settings) +} + +// SaveSiteSettings 保存站点全局配置,包含标题、LOGO、ico、SEO、GSO/GEO 和高德地图配置。 +func (s AdminService) SaveSiteSettings(settings model.SiteSettings) model.SiteSettings { + adminStore.Lock() + defer adminStore.Unlock() + if settings.DefaultSeo == nil { + settings.DefaultSeo = map[string]interface{}{} + } + if settings.DefaultGso == nil { + settings.DefaultGso = map[string]interface{}{} + } + adminStore.settings = copySettings(settings) + return copySettings(adminStore.settings) +} + +func defaultSiteSettings() model.SiteSettings { + return model.SiteSettings{ + SiteTitle: "萧康云医官网", + SiteLogo: "/src/assets/images/logo.png", + SiteLogoDark: "/src/assets/images/logo-white.png", + SiteFavicon: "/favicon.ico", + IcpText: "浙ICP备0000000号", + Copyright: "Copyright © 萧康云医", + ContactPhone: "0571-00000000", + ContactEmail: "contact@xiaokang.example", + ContactAddress: "杭州市萧山区鸿盛路与高新六路交叉口东200米", + DefaultSeo: map[string]interface{}{ + "title": "萧康云医官网", + "keywords": "诊所SaaS,云医,诊所管理", + "description": "萧康云医为诊所提供获客、预约、接诊、复诊和经营分析能力。", + }, + DefaultGso: map[string]interface{}{ + "brand": "萧康云医", + "summary": "面向诊所赋能的 SaaS 平台", + "services": []string{"患者端", "医生端", "老板端", "推广员端", "PC后台"}, + }, + AmapKey: "", + AmapSecurityCode: "", + } +} + +func defaultSitePages() []model.SitePage { + return []model.SitePage{ + {Id: 1, Code: "home", Title: "首页", SeoTitle: "萧康云医官网", SeoKeywords: "诊所SaaS,诊所管理,云医", SeoDescription: "萧康云医为诊所提供获客、预约、接诊、复诊和经营分析能力。", GsoContent: map[string]interface{}{"summary": "面向诊所赋能的 SaaS 平台", "entities": []string{"萧康云医", "诊所SaaS"}}, Status: 1, Sort: 1}, + {Id: 2, Code: "features", Title: "产品能力", SeoTitle: "萧康云医产品能力", SeoKeywords: "患者端,医生端,推广员端,PC后台", SeoDescription: "了解萧康云医患者端、医生端、老板端、推广员端和后台能力。", GsoContent: map[string]interface{}{"summary": "多端协同产品能力"}, Status: 1, Sort: 2}, + {Id: 3, Code: "pricing", Title: "价格方案", SeoTitle: "萧康云医价格", SeoKeywords: "SaaS定价,诊所套餐", SeoDescription: "查看萧康云医基础版、增长版、连锁版套餐。", GsoContent: map[string]interface{}{"summary": "诊所 SaaS 套餐价格"}, Status: 1, Sort: 3}, + {Id: 4, Code: "careers", Title: "加入我们", SeoTitle: "加入萧康云医", SeoKeywords: "招聘,医疗SaaS招聘", SeoDescription: "加入萧康云医团队,共建诊所赋能平台。", GsoContent: map[string]interface{}{"summary": "萧康云医招聘职位"}, Status: 1, Sort: 4}, + } +} + +func defaultSiteSections() []model.SiteSection { + return []model.SiteSection{ + {Id: 1, PageCode: "features", SectionKey: "hero", Title: "能力地图", Subtitle: "多端协同与经营后台", Content: map[string]interface{}{"layout": "capability_map", "image": "/src/assets/images/features0001.jpeg"}, Status: 1, Sort: 1}, + {Id: 2, PageCode: "features", SectionKey: "workflow", Title: "流程切片", Subtitle: "获客到复诊", Content: map[string]interface{}{"steps": []string{"获客", "预约", "接诊", "复诊", "复盘"}}, Status: 1, Sort: 2}, + {Id: 3, PageCode: "pricing", SectionKey: "plans", Title: "套餐矩阵", Subtitle: "月付/年付与权益对比", Content: map[string]interface{}{"billing": []string{"month", "year"}}, Status: 1, Sort: 1}, + } +} + +func defaultSiteMedia() []model.SiteMedia { + return []model.SiteMedia{ + {Id: 1, MediaType: "logo", GroupCode: "brand", Title: "站点LOGO", Url: "/src/assets/images/logo.png", Alt: "萧康云医LOGO", Status: 1, Sort: 1}, + {Id: 2, MediaType: "favicon", GroupCode: "brand", Title: "favicon", Url: "/favicon.ico", Alt: "萧康云医图标", Status: 1, Sort: 2}, + {Id: 3, MediaType: "page_image", GroupCode: "features", Title: "产品能力首图", Url: "/src/assets/images/features0001.jpeg", Alt: "萧康云医产品能力", Status: 1, Sort: 3}, + {Id: 4, MediaType: "share_image", GroupCode: "seo", Title: "默认分享图", Url: "/src/assets/images/home0001.jpeg", Alt: "萧康云医官网分享图", Status: 1, Sort: 4}, + } +} + +func defaultRecruitJobs() []model.RecruitJob { + return []model.RecruitJob{ + {Id: 1, Title: "高级前端开发工程师", Department: "技术部", JobType: "全职", City: "杭州", Salary: "25K-45K", Experience: "5年以上", Description: "负责萧康云医官网、后台和小程序端前端架构与体验优化。", Requirements: []string{"熟悉 Vue 3 与工程化", "有中后台或 SaaS 项目经验", "关注性能、可访问性和组件化"}, Manager: "张经理", ManagerTitle: "技术负责人", Email: "tech@xiaokang.example", Phone: "0571-00000000", Featured: true, Status: 1, Sort: 1, PublishedDate: "2026-07-05"}, + {Id: 2, Title: "诊所运营顾问", Department: "运营部", JobType: "全职", City: "杭州", Salary: "12K-22K", Experience: "3年以上", Description: "面向诊所客户完成系统演示、上线辅导和经营复盘。", Requirements: []string{"熟悉医疗或本地生活服务行业", "具备客户培训和项目推进能力", "表达清晰,能沉淀方法论"}, Manager: "王经理", ManagerTitle: "运营负责人", Email: "ops@xiaokang.example", Phone: "0571-00000001", Featured: false, Status: 1, Sort: 2, PublishedDate: "2026-07-05"}, + } +} + +func copyMap(source map[string]interface{}) map[string]interface{} { + if source == nil { + return map[string]interface{}{} + } + result := make(map[string]interface{}, len(source)) + for key, value := range source { + result[key] = value + } + return result +} + +func copySettings(settings model.SiteSettings) model.SiteSettings { + settings.DefaultSeo = copyMap(settings.DefaultSeo) + settings.DefaultGso = copyMap(settings.DefaultGso) + return settings +} + +func copyPages(pages []model.SitePage) []model.SitePage { + items := append([]model.SitePage(nil), pages...) + sort.SliceStable(items, func(i, j int) bool { + return items[i].Sort < items[j].Sort || items[i].Sort == items[j].Sort && items[i].Id < items[j].Id + }) + return items +} + +func upsertPage(items []model.SitePage, item model.SitePage) []model.SitePage { + for index := range items { + if items[index].Id == item.Id || item.Code != "" && items[index].Code == item.Code { + items[index] = item + return items + } + } + return append(items, item) +} + +func upsertSection(items []model.SiteSection, item model.SiteSection) []model.SiteSection { + for index := range items { + if items[index].Id == item.Id || item.SectionKey != "" && items[index].PageCode == item.PageCode && items[index].SectionKey == item.SectionKey { + items[index] = item + return items + } + } + return append(items, item) +} + +func upsertMedia(items []model.SiteMedia, item model.SiteMedia) []model.SiteMedia { + for index := range items { + if items[index].Id == item.Id { + items[index] = item + return items + } + } + return append(items, item) +} + +func upsertJob(items []model.RecruitJob, item model.RecruitJob) []model.RecruitJob { + for index := range items { + if items[index].Id == item.Id { + items[index] = item + return items + } + } + return append(items, item) +} + +func sortSections(items []model.SiteSection) { + sort.SliceStable(items, func(i, j int) bool { + return items[i].Sort < items[j].Sort || items[i].Sort == items[j].Sort && items[i].Id < items[j].Id + }) +} + +func sortMedia(items []model.SiteMedia) { + sort.SliceStable(items, func(i, j int) bool { + return items[i].Sort < items[j].Sort || items[i].Sort == items[j].Sort && items[i].Id < items[j].Id + }) +} + +func sortJobs(items []model.RecruitJob) { + sort.SliceStable(items, func(i, j int) bool { + return items[i].Sort < items[j].Sort || items[i].Sort == items[j].Sort && items[i].Id < items[j].Id + }) +} + +func nextPageId(items []model.SitePage) int { + maxId := 0 + for _, item := range items { + if item.Id > maxId { + maxId = item.Id + } + } + return maxId + 1 +} + +func nextSectionId(items []model.SiteSection) int { + maxId := 0 + for _, item := range items { + if item.Id > maxId { + maxId = item.Id + } + } + return maxId + 1 +} + +func nextMediaId(items []model.SiteMedia) int { + maxId := 0 + for _, item := range items { + if item.Id > maxId { + maxId = item.Id + } + } + return maxId + 1 +} + +func nextJobId(items []model.RecruitJob) int { + maxId := 0 + for _, item := range items { + if item.Id > maxId { + maxId = item.Id + } + } + return maxId + 1 +} diff --git a/internal/service/lead.go b/internal/service/lead.go new file mode 100644 index 0000000..8d42b62 --- /dev/null +++ b/internal/service/lead.go @@ -0,0 +1,33 @@ +package service + +import ( + "errors" + "strings" + + "xk-of-api/internal/model" +) + +// LeadService 负责官网线索校验和保存,后续接入 nl_site_lead。 +type LeadService struct{} + +// Lead 暴露线索服务单例。 +var Lead = LeadService{} + +// Create 校验并创建咨询线索。 +func (s LeadService) Create(lead model.Lead) (model.Lead, error) { + if strings.TrimSpace(lead.Name) == "" { + return model.Lead{}, errors.New("请填写联系人姓名") + } + if strings.TrimSpace(lead.Phone) == "" { + return model.Lead{}, errors.New("请填写联系电话") + } + if lead.SourcePage == "" { + lead.SourcePage = "website" + } + return lead, nil +} + +// List 返回后台线索列表占位数据。 +func (s LeadService) List() []model.Lead { + return []model.Lead{{Name: "王医生", Phone: "13800000000", ClinicName: "萧康示例诊所", City: "上海", PlanCode: "growth", SourcePage: "pricing", Remark: "希望了解增长版"}} +} diff --git a/internal/service/order.go b/internal/service/order.go new file mode 100644 index 0000000..d73885e --- /dev/null +++ b/internal/service/order.go @@ -0,0 +1,50 @@ +package service + +import ( + "errors" + "fmt" + "time" + + "xk-of-api/internal/model" +) + +// OrderService 负责套餐订购和订单状态查询。 +type OrderService struct{} + +// Order 暴露订单服务单例。 +var Order = OrderService{} + +// Create 创建套餐订单;金额根据套餐和周期计算。 +func (s OrderService) Create(order model.Order) (model.Order, error) { + if order.TenantName == "" || order.ContactName == "" || order.Phone == "" { + return model.Order{}, errors.New("请补全诊所名称、联系人和手机号") + } + plan, ok := Pricing.FindByCode(order.PlanCode) + if !ok { + return model.Order{}, errors.New("套餐不存在") + } + if order.BillingCycle == "year" { + order.Amount = plan.YearlyFee + } else { + order.BillingCycle = "month" + order.Amount = plan.MonthlyFee + } + order.OrderNo = fmt.Sprintf("XK%d", time.Now().UnixNano()) + order.Status = "pending" + return order, nil +} + +// Detail 返回订单详情占位数据,后续改为读取 nl_order。 +func (s OrderService) Detail(orderNo string) model.Order { + return model.Order{OrderNo: orderNo, TenantName: "萧康示例诊所", ContactName: "王医生", Phone: "13800000000", PlanCode: "growth", BillingCycle: "year", Amount: 399000, Status: "pending"} +} + +// List 返回后台订单列表占位数据。 +func (s OrderService) List() []model.Order { + return []model.Order{s.Detail("XK202607040001")} +} + +// Notify 处理支付渠道回调占位逻辑,真实渠道验签在后续适配器完成。 +func (s OrderService) Notify(channel string, payload map[string]interface{}) map[string]interface{} { + return map[string]interface{}{"channel": channel, "notify_status": "received", "payload": payload} +} diff --git a/internal/service/pricing.go b/internal/service/pricing.go new file mode 100644 index 0000000..da7efba --- /dev/null +++ b/internal/service/pricing.go @@ -0,0 +1,28 @@ +package service + +import "xk-of-api/internal/model" + +// PricingService 负责 SaaS 套餐展示和套餐查询。 +type PricingService struct{} + +// Pricing 暴露定价服务单例。 +var Pricing = PricingService{} + +// Plans 返回官网定价套餐,后续可由 nl_saas_plan 和 nl_saas_plan_feature 驱动。 +func (s PricingService) Plans() []model.Plan { + return []model.Plan{ + {Id: 1, Code: "starter", Name: "基础版", Description: "适合单店诊所快速上线线上预约和基础经营管理。", MonthlyFee: 19900, YearlyFee: 199000, Recommended: false, Features: []model.PlanFeature{{Name: "患者端小程序", Value: "基础预约与提醒", Highlight: true}, {Name: "PC后台", Value: "线索与订单管理", Highlight: true}, {Name: "账号数量", Value: "5个员工账号"}}}, + {Id: 2, Code: "growth", Name: "增长版", Description: "适合需要推广员获客、医生协同和经营分析的成长型诊所。", MonthlyFee: 39900, YearlyFee: 399000, Recommended: true, Features: []model.PlanFeature{{Name: "四端小程序", Value: "患者/医生/老板/推广员", Highlight: true}, {Name: "经营看板", Value: "收入、转化、复购分析", Highlight: true}, {Name: "账号数量", Value: "20个员工账号"}}}, + {Id: 3, Code: "enterprise", Name: "连锁版", Description: "适合连锁门店和需要深度配置的诊所集团。", MonthlyFee: 89900, YearlyFee: 899000, Recommended: false, Features: []model.PlanFeature{{Name: "多门店能力", Value: "预留扩展与独立配置", Highlight: true}, {Name: "专属服务", Value: "部署辅导和运营顾问", Highlight: true}, {Name: "账号数量", Value: "不限账号数量"}}}, + } +} + +// FindByCode 根据套餐编码查找套餐。 +func (s PricingService) FindByCode(code string) (model.Plan, bool) { + for _, plan := range s.Plans() { + if plan.Code == code { + return plan, true + } + } + return model.Plan{}, false +} diff --git a/internal/service/site.go b/internal/service/site.go new file mode 100644 index 0000000..bddb6e5 --- /dev/null +++ b/internal/service/site.go @@ -0,0 +1,59 @@ +package service + +import "xk-of-api/internal/model" + +// SiteService 负责官网公开内容聚合,后续可替换为数据库读取 nl_site_page/nl_site_section。 +type SiteService struct{} + +// Site 暴露官网服务单例,控制器只调用服务层。 +var Site = SiteService{} + +// Home 返回萧康云医官网首页聚合数据。 +func (s SiteService) Home() model.SiteHome { + return model.SiteHome{ + Hero: model.HeroBlock{ + Title: "萧康云医,让诊所经营更清楚、更稳定、更可增长", + Subtitle: "连接患者、医生、门店老板、推广员和 PC 管理后台,用一套 SaaS 平台完成获客、预约、接诊、复诊、经营分析和团队协同。", + PrimaryCTA: "预约演示", + SecondaryCTA: "查看定价", + Stats: []model.StatBlock{ + {Value: "4端", Label: "小程序角色覆盖"}, + {Value: "1套", Label: "PC 经营后台"}, + {Value: "7x24", Label: "线上服务触达"}, + }, + }, + Products: []model.ProductBlock{ + {Name: "患者端", Role: "患者", Description: "预约、复诊、健康档案和服务提醒统一承载。", Features: []string{"在线预约", "复诊提醒", "服务记录"}}, + {Name: "医生端", Role: "医生", Description: "帮助医生管理接诊流程、患者跟进和医嘱记录。", Features: []string{"患者管理", "接诊记录", "随访任务"}}, + {Name: "门店老板端", Role: "经营者", Description: "实时查看门店经营、转化、复购和团队执行情况。", Features: []string{"经营看板", "员工绩效", "收入分析"}}, + {Name: "推广员端", Role: "推广员", Description: "追踪推广码、线索归属和成交转化,降低获客管理成本。", Features: []string{"推广码", "线索归属", "佣金统计"}}, + {Name: "PC 后台", Role: "管理员", Description: "完成权限、菜单、套餐、订单、线索和基础配置管理。", Features: []string{"权限菜单", "套餐管理", "订单管理"}}, + }, + Highlights: []model.ValueBlock{ + {Title: "诊所全链路赋能", Description: "从获客、预约、接诊到复诊运营,减少系统割裂。", Icon: "fa-solid fa-route"}, + {Title: "多角色协同", Description: "不同角色使用独立端口,数据统一汇总到经营后台。", Icon: "fa-solid fa-users-gear"}, + {Title: "经营数据沉淀", Description: "沉淀线索、订单、患者服务和团队执行数据。", Icon: "fa-solid fa-chart-line"}, + }, + Faqs: []model.FaqBlock{ + {Question: "适合哪些诊所?", Answer: "适合希望提升线上获客、复诊运营和经营管理效率的综合诊所、专科诊所和连锁门店。"}, + {Question: "是否支持多门店?", Answer: "一期先保留租户主体,后续可扩展门店、员工和分账等完整经营表。"}, + {Question: "可以先预约演示吗?", Answer: "可以,官网线索会进入后台,运营人员可跟进套餐和部署方案。"}, + }, + } +} + +// Settings 返回前台可公开使用的站点配置,包含标题、LOGO、SEO/GSO 和地图配置。 +func (s SiteService) Settings() model.SiteSettings { + return Admin.SiteSettings() +} + +// RecruitJobs 返回前台启用状态的招聘职位。 +func (s SiteService) RecruitJobs() []model.RecruitJob { + jobs := make([]model.RecruitJob, 0) + for _, job := range Admin.RecruitJobs() { + if job.Status == 1 { + jobs = append(jobs, job) + } + } + return jobs +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..cd06198 --- /dev/null +++ b/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/gogf/gf/v2/os/gctx" + + "xk-of-api/internal/cmd" +) + +func main() { + cmd.Main.Run(gctx.GetInitCtx()) +} diff --git a/manifest/deploy/kustomize/base/deployment.yaml b/manifest/deploy/kustomize/base/deployment.yaml new file mode 100644 index 0000000..28f1d69 --- /dev/null +++ b/manifest/deploy/kustomize/base/deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: template-single + labels: + app: template-single +spec: + replicas: 1 + selector: + matchLabels: + app: template-single + template: + metadata: + labels: + app: template-single + spec: + containers: + - name : main + image: template-single + imagePullPolicy: Always + diff --git a/manifest/deploy/kustomize/base/kustomization.yaml b/manifest/deploy/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..302d92d --- /dev/null +++ b/manifest/deploy/kustomize/base/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- deployment.yaml +- service.yaml + + + diff --git a/manifest/deploy/kustomize/base/service.yaml b/manifest/deploy/kustomize/base/service.yaml new file mode 100644 index 0000000..608771c --- /dev/null +++ b/manifest/deploy/kustomize/base/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: template-single +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 8000 + selector: + app: template-single + diff --git a/manifest/deploy/kustomize/overlays/develop/configmap.yaml b/manifest/deploy/kustomize/overlays/develop/configmap.yaml new file mode 100644 index 0000000..3b1d0af --- /dev/null +++ b/manifest/deploy/kustomize/overlays/develop/configmap.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: template-single-configmap +data: + config.yaml: | + server: + address: ":8000" + openapiPath: "/api.json" + swaggerPath: "/swagger" + + logger: + level : "all" + stdout: true diff --git a/manifest/deploy/kustomize/overlays/develop/deployment.yaml b/manifest/deploy/kustomize/overlays/develop/deployment.yaml new file mode 100644 index 0000000..04e4851 --- /dev/null +++ b/manifest/deploy/kustomize/overlays/develop/deployment.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: template-single +spec: + template: + spec: + containers: + - name : main + image: template-single:develop \ No newline at end of file diff --git a/manifest/deploy/kustomize/overlays/develop/kustomization.yaml b/manifest/deploy/kustomize/overlays/develop/kustomization.yaml new file mode 100644 index 0000000..4731c47 --- /dev/null +++ b/manifest/deploy/kustomize/overlays/develop/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../base +- configmap.yaml + +patchesStrategicMerge: +- deployment.yaml + +namespace: default + + + diff --git a/manifest/docker/Dockerfile b/manifest/docker/Dockerfile new file mode 100644 index 0000000..d3abe8f --- /dev/null +++ b/manifest/docker/Dockerfile @@ -0,0 +1,16 @@ +FROM loads/alpine:3.8 + +############################################################################### +# INSTALLATION +############################################################################### + +ENV WORKDIR /app +ADD resource $WORKDIR/ +ADD ./temp/linux_amd64/main $WORKDIR/main +RUN chmod +x $WORKDIR/main + +############################################################################### +# START +############################################################################### +WORKDIR $WORKDIR +CMD ./main diff --git a/manifest/docker/docker.sh b/manifest/docker/docker.sh new file mode 100644 index 0000000..ff393f9 --- /dev/null +++ b/manifest/docker/docker.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# This shell is executed before docker build. + + + + + diff --git a/manifest/i18n/.gitkeep b/manifest/i18n/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/manifest/protobuf/.keep-if-necessary b/manifest/protobuf/.keep-if-necessary new file mode 100644 index 0000000..e69de29 diff --git a/manifest/sql/xk_of_schema.sql b/manifest/sql/xk_of_schema.sql new file mode 100644 index 0000000..60602c0 --- /dev/null +++ b/manifest/sql/xk_of_schema.sql @@ -0,0 +1,371 @@ +CREATE DATABASE IF NOT EXISTS `xk_of` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '萧康云医官网与SaaS后台数据库'; + +USE `xk_of`; + +CREATE TABLE IF NOT EXISTS `nl_admin` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `open_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OpenID,后期整合萧康服务中心会用到', + `wx_open_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '微信小程序OpenID', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '头像', + `nick_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵称', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', + `phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户手机', + `email` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱', + `code` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '业务员推广码', + `role_id` int NOT NULL DEFAULT '0' COMMENT '角色', + `province_id` int NOT NULL DEFAULT '0' COMMENT '省', + `city_id` int NOT NULL DEFAULT '0' COMMENT '市', + `reg_ip` bigint NOT NULL DEFAULT '0' COMMENT '注册IP', + `last_login_time` int NOT NULL DEFAULT '0' COMMENT '最后登录时间', + `ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '0' COMMENT '最后登录IP', + `ip_table` json NOT NULL COMMENT '常用登录IP地址列表', + `operation_password` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '操作密码', + `desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '用户状态 0正常 1禁用', + `last_reset_password_at` datetime DEFAULT NULL COMMENT '最后一次更新密码时间', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_phone` (`phone`) USING BTREE, + KEY `idx_role_id` (`role_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='管理员表'; + +CREATE TABLE IF NOT EXISTS `nl_role` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '角色ID', + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '角色名称', + `code` varchar(64) NOT NULL DEFAULT '' COMMENT '角色编码', + `desc` varchar(255) NOT NULL DEFAULT '' COMMENT '角色说明', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_code` (`code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表'; + +CREATE TABLE IF NOT EXISTS `nl_admin_role` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '关联ID', + `admin_id` int unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID', + `role_id` int unsigned NOT NULL DEFAULT '0' COMMENT '角色ID', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_admin_role` (`admin_id`,`role_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='管理员角色关联表'; + +CREATE TABLE IF NOT EXISTS `nl_menu` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `parent_id` int unsigned NOT NULL DEFAULT '0' COMMENT '父级菜单ID', + `title` varchar(64) NOT NULL DEFAULT '' COMMENT '菜单标题', + `name` varchar(64) NOT NULL DEFAULT '' COMMENT '路由名称', + `path` varchar(255) NOT NULL DEFAULT '' COMMENT '路由路径', + `component` varchar(255) NOT NULL DEFAULT '' COMMENT '前端组件路径', + `redirect` varchar(255) NOT NULL DEFAULT '' COMMENT '重定向地址', + `icon` varchar(64) NOT NULL DEFAULT '' COMMENT '菜单图标', + `type` tinyint NOT NULL DEFAULT '1' COMMENT '菜单类型 1目录 2菜单 3按钮', + `permission` varchar(128) NOT NULL DEFAULT '' COMMENT '权限标识', + `visible` tinyint NOT NULL DEFAULT '1' COMMENT '是否显示 0隐藏 1显示', + `keep_alive` tinyint NOT NULL DEFAULT '0' COMMENT '是否缓存页面 0否 1是', + `menu_area` tinyint NOT NULL DEFAULT '1' COMMENT '双列菜单区域 1主列 2次列', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_parent_id` (`parent_id`) USING BTREE, + KEY `idx_permission` (`permission`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='后台多级菜单表'; + +CREATE TABLE IF NOT EXISTS `nl_role_menu` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '授权ID', + `role_id` int unsigned NOT NULL DEFAULT '0' COMMENT '角色ID', + `menu_id` int unsigned NOT NULL DEFAULT '0' COMMENT '菜单ID', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_role_menu` (`role_id`,`menu_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色菜单授权表'; + +CREATE TABLE IF NOT EXISTS `nl_system_config` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `config_key` varchar(128) NOT NULL DEFAULT '' COMMENT '配置键', + `config_value` text NOT NULL COMMENT '配置值', + `desc` varchar(255) NOT NULL DEFAULT '' COMMENT '配置说明', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_config_key` (`config_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统配置表'; + +CREATE TABLE IF NOT EXISTS `nl_admin_preference` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '偏好ID', + `admin_id` int unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID', + `menu_layout` varchar(16) NOT NULL DEFAULT '' COMMENT '菜单布局 single单列 double双列 空则使用全局配置', + `theme` varchar(16) NOT NULL DEFAULT '' COMMENT '后台主题 light明亮 dark暗色 空则跟随系统', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_admin_id` (`admin_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='管理员个人偏好表'; + +CREATE TABLE IF NOT EXISTS `nl_site_page` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '页面ID', + `code` varchar(64) NOT NULL DEFAULT '' COMMENT '页面编码', + `title` varchar(128) NOT NULL DEFAULT '' COMMENT '页面标题', + `seo_title` varchar(255) NOT NULL DEFAULT '' COMMENT 'SEO标题', + `seo_keywords` varchar(255) NOT NULL DEFAULT '' COMMENT 'SEO关键词', + `seo_description` varchar(500) NOT NULL DEFAULT '' COMMENT 'SEO描述', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_code` (`code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='官网页面表'; + +CREATE TABLE IF NOT EXISTS `nl_site_section` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '模块ID', + `page_id` int unsigned NOT NULL DEFAULT '0' COMMENT '页面ID', + `section_key` varchar(64) NOT NULL DEFAULT '' COMMENT '模块标识', + `title` varchar(128) NOT NULL DEFAULT '' COMMENT '模块标题', + `subtitle` varchar(255) NOT NULL DEFAULT '' COMMENT '模块副标题', + `content` json NOT NULL COMMENT '模块内容JSON', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_page_id` (`page_id`) USING BTREE, + KEY `idx_section_key` (`section_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='官网页面模块表'; + +CREATE TABLE IF NOT EXISTS `nl_site_lead` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '线索ID', + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '联系人姓名', + `phone` char(11) NOT NULL DEFAULT '' COMMENT '联系电话', + `clinic_name` varchar(128) NOT NULL DEFAULT '' COMMENT '诊所名称', + `city` varchar(64) NOT NULL DEFAULT '' COMMENT '所在城市', + `plan_code` varchar(64) NOT NULL DEFAULT '' COMMENT '意向套餐编码', + `source_page` varchar(64) NOT NULL DEFAULT '' COMMENT '来源页面', + `remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注', + `follow_status` tinyint NOT NULL DEFAULT '0' COMMENT '跟进状态 0待跟进 1跟进中 2已成交 3无效', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_phone` (`phone`) USING BTREE, + KEY `idx_plan_code` (`plan_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='官网咨询线索表'; + +CREATE TABLE IF NOT EXISTS `nl_saas_plan` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '套餐ID', + `code` varchar(64) NOT NULL DEFAULT '' COMMENT '套餐编码', + `name` varchar(64) NOT NULL DEFAULT '' COMMENT '套餐名称', + `description` varchar(500) NOT NULL DEFAULT '' COMMENT '套餐说明', + `monthly_fee` int unsigned NOT NULL DEFAULT '0' COMMENT '月付金额,单位分', + `yearly_fee` int unsigned NOT NULL DEFAULT '0' COMMENT '年付金额,单位分', + `recommended` tinyint NOT NULL DEFAULT '0' COMMENT '是否推荐 0否 1是', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_code` (`code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='SaaS套餐表'; + +CREATE TABLE IF NOT EXISTS `nl_saas_plan_feature` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '权益ID', + `plan_id` int unsigned NOT NULL DEFAULT '0' COMMENT '套餐ID', + `name` varchar(64) NOT NULL DEFAULT '' COMMENT '权益名称', + `value` varchar(255) NOT NULL DEFAULT '' COMMENT '权益值', + `highlight` tinyint NOT NULL DEFAULT '0' COMMENT '是否重点展示 0否 1是', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_plan_id` (`plan_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='SaaS套餐权益表'; + +CREATE TABLE IF NOT EXISTS `nl_tenant` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '租户ID', + `tenant_name` varchar(128) NOT NULL DEFAULT '' COMMENT '租户名称或诊所名称', + `contact_name` varchar(32) NOT NULL DEFAULT '' COMMENT '联系人姓名', + `phone` char(11) NOT NULL DEFAULT '' COMMENT '联系电话', + `city` varchar(64) NOT NULL DEFAULT '' COMMENT '所在城市', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_phone` (`phone`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='SaaS订购主体表'; + +CREATE TABLE IF NOT EXISTS `nl_subscription` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '订阅ID', + `tenant_id` int unsigned NOT NULL DEFAULT '0' COMMENT '租户ID', + `plan_id` int unsigned NOT NULL DEFAULT '0' COMMENT '套餐ID', + `start_at` int NOT NULL DEFAULT '0' COMMENT '订阅开始时间', + `end_at` int NOT NULL DEFAULT '0' COMMENT '订阅结束时间', + `auto_renew` tinyint NOT NULL DEFAULT '0' COMMENT '是否自动续费 0否 1是', + `subscription_status` tinyint NOT NULL DEFAULT '0' COMMENT '订阅状态 0待生效 1生效中 2已过期 3已取消', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_plan_id` (`plan_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='SaaS订阅关系表'; + +CREATE TABLE IF NOT EXISTS `nl_order` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '订单ID', + `order_no` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号', + `tenant_id` int unsigned NOT NULL DEFAULT '0' COMMENT '租户ID', + `plan_id` int unsigned NOT NULL DEFAULT '0' COMMENT '套餐ID', + `billing_cycle` varchar(16) NOT NULL DEFAULT 'month' COMMENT '订购周期 month月付 year年付', + `amount` int unsigned NOT NULL DEFAULT '0' COMMENT '订单金额,单位分', + `pay_status` tinyint NOT NULL DEFAULT '0' COMMENT '支付状态 0待支付 1已支付 2已退款 3已关闭', + `order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态 0待支付 1已完成 2已取消 3已关闭', + `expired_at` int NOT NULL DEFAULT '0' COMMENT '订单过期时间', + `paid_at` int NOT NULL DEFAULT '0' COMMENT '支付完成时间', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_order_no` (`order_no`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`) USING BTREE, + KEY `idx_plan_id` (`plan_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='SaaS套餐订购订单表'; + +CREATE TABLE IF NOT EXISTS `nl_payment` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '支付ID', + `order_no` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号', + `channel` varchar(32) NOT NULL DEFAULT '' COMMENT '支付渠道 wxpay微信 alipay支付宝 manual线下', + `channel_trade_no` varchar(128) NOT NULL DEFAULT '' COMMENT '渠道交易号', + `amount` int unsigned NOT NULL DEFAULT '0' COMMENT '支付金额,单位分', + `pay_status` tinyint NOT NULL DEFAULT '0' COMMENT '支付状态 0待支付 1成功 2失败 3退款', + `notify_payload` json NOT NULL COMMENT '支付回调原始数据', + `paid_at` int NOT NULL DEFAULT '0' COMMENT '支付完成时间', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_order_no` (`order_no`) USING BTREE, + KEY `idx_channel_trade_no` (`channel_trade_no`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='SaaS支付流水表'; + +INSERT INTO `nl_system_config` (`config_key`, `config_value`, `desc`, `status`, `created_at`, `updated_at`) +VALUES ('admin_menu_layout', 'double', '后台默认菜单布局 single单列 double双列', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE `config_value` = VALUES(`config_value`), `updated_at` = UNIX_TIMESTAMP(); + +INSERT INTO `nl_role` (`id`, `name`, `code`, `desc`, `status`, `sort`, `created_at`, `updated_at`) +VALUES + (1, '超级管理员', 'super_admin', '拥有后台全部权限', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (2, '运营人员', 'operator', '管理官网内容、线索、套餐和订单', 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `updated_at` = UNIX_TIMESTAMP(); + +INSERT INTO `nl_menu` (`id`, `parent_id`, `title`, `name`, `path`, `component`, `icon`, `type`, `permission`, `visible`, `menu_area`, `status`, `sort`, `created_at`, `updated_at`) +VALUES + (1, 0, '控制台', 'Dashboard', '/dashboard', 'Dashboard', 'dashboard', 2, 'dashboard:view', 1, 1, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (2, 0, '官网运营', 'Site', '/site', 'Layout', 'global', 1, 'site:view', 1, 1, 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (21, 2, '页面模块', 'SiteSections', '/site/sections', 'SiteSections', 'blocks', 2, 'site:section:view', 1, 2, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (22, 2, '咨询线索', 'SiteLeads', '/site/leads', 'SiteLeads', 'message', 2, 'site:lead:view', 1, 2, 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (3, 0, '商业化', 'Commerce', '/commerce', 'Layout', 'wallet', 1, 'commerce:view', 1, 1, 1, 3, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (31, 3, '套餐管理', 'PlanList', '/commerce/plans', 'PlanList', 'package', 2, 'plan:view', 1, 2, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (32, 3, '订单管理', 'OrderList', '/commerce/orders', 'OrderList', 'orders', 2, 'order:view', 1, 2, 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (4, 0, '系统管理', 'System', '/system', 'Layout', 'setting', 1, 'system:view', 1, 1, 1, 4, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (41, 4, '管理员', 'AdminList', '/system/admins', 'AdminList', 'user', 2, 'admin:view', 1, 2, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (42, 4, '角色权限', 'RoleList', '/system/roles', 'RoleList', 'safety', 2, 'role:view', 1, 2, 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (43, 4, '菜单管理', 'MenuList', '/system/menus', 'MenuList', 'menu', 2, 'menu:view', 1, 2, 1, 3, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `updated_at` = UNIX_TIMESTAMP(); + +INSERT INTO `nl_saas_plan` (`id`, `code`, `name`, `description`, `monthly_fee`, `yearly_fee`, `recommended`, `status`, `sort`, `created_at`, `updated_at`) +VALUES + (1, 'starter', '基础版', '适合单店诊所快速上线线上预约和基础经营管理', 19900, 199000, 0, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (2, 'growth', '增长版', '适合需要推广员获客、医生协同和经营分析的成长型诊所', 39900, 399000, 1, 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (3, 'enterprise', '连锁版', '适合连锁门店和需要深度配置的诊所集团', 89900, 899000, 0, 1, 3, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `updated_at` = UNIX_TIMESTAMP(); + +INSERT INTO `nl_saas_plan_feature` (`id`, `plan_id`, `name`, `value`, `highlight`, `status`, `sort`, `created_at`, `updated_at`) +VALUES + (1, 1, '患者端小程序', '基础预约与提醒', 1, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (2, 1, 'PC后台', '线索与订单管理', 1, 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (3, 1, '账号数量', '5个员工账号', 0, 1, 3, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (4, 2, '四端小程序', '患者端、医生端、老板端、推广员端', 1, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (5, 2, '经营看板', '收入、转化、复购分析', 1, 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (6, 2, '账号数量', '20个员工账号', 0, 1, 3, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (7, 3, '多门店能力', '预留连锁门店扩展', 1, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (8, 3, '专属服务', '部署辅导和运营顾问', 1, 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (9, 3, '账号数量', '不限账号数量', 0, 1, 3, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `updated_at` = UNIX_TIMESTAMP(); + +-- 官网 CMS 扩展:页面 GSO/GEO、素材、招聘、站点配置 +ALTER TABLE `nl_site_page` ADD COLUMN `gso_content` json DEFAULT NULL COMMENT 'GSO/GEO生成式搜索优化内容JSON' AFTER `seo_description`; + +CREATE TABLE IF NOT EXISTS `nl_site_media` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '素材ID', + `media_type` varchar(32) NOT NULL DEFAULT '' COMMENT '素材类型 logo标志 favicon站点图标 page_image页面图片 share_image分享图 job_image招聘图', + `group_code` varchar(64) NOT NULL DEFAULT '' COMMENT '素材分组编码', + `title` varchar(128) NOT NULL DEFAULT '' COMMENT '素材标题', + `url` varchar(500) NOT NULL DEFAULT '' COMMENT '素材地址', + `alt` varchar(255) NOT NULL DEFAULT '' COMMENT '图片替代文本', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_media_type` (`media_type`) USING BTREE, + KEY `idx_group_code` (`group_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='官网素材表'; + +CREATE TABLE IF NOT EXISTS `nl_recruit_job` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '职位ID', + `title` varchar(128) NOT NULL DEFAULT '' COMMENT '职位名称', + `department` varchar(64) NOT NULL DEFAULT '' COMMENT '所属部门', + `job_type` varchar(32) NOT NULL DEFAULT '' COMMENT '职位类型', + `city` varchar(64) NOT NULL DEFAULT '' COMMENT '工作城市', + `salary` varchar(64) NOT NULL DEFAULT '' COMMENT '薪资范围', + `experience` varchar(64) NOT NULL DEFAULT '' COMMENT '经验要求', + `description` varchar(1000) NOT NULL DEFAULT '' COMMENT '职位描述', + `requirements` json NOT NULL COMMENT '岗位要求JSON', + `manager` varchar(32) NOT NULL DEFAULT '' COMMENT '招聘负责人', + `manager_title` varchar(64) NOT NULL DEFAULT '' COMMENT '负责人职位', + `email` varchar(128) NOT NULL DEFAULT '' COMMENT '投递邮箱', + `phone` varchar(32) NOT NULL DEFAULT '' COMMENT '联系电话', + `featured` tinyint NOT NULL DEFAULT '0' COMMENT '是否重点招聘 0否 1是', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0下架 1上架', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序', + `created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_department` (`department`) USING BTREE, + KEY `idx_status_sort` (`status`,`sort`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='招聘职位表'; + +INSERT INTO `nl_system_config` (`config_key`, `config_value`, `desc`, `status`, `created_at`, `updated_at`) +VALUES + ('site_title', '萧康云医官网', '站点标题', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + ('site_logo', '/src/assets/images/logo.png', '站点LOGO', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + ('site_favicon', '/favicon.ico', '站点favicon图标', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + ('site_seo', '{"title":"萧康云医官网","keywords":"诊所SaaS,云医,诊所管理","description":"萧康云医为诊所提供全链路经营赋能。"}', '默认SEO配置JSON', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + ('site_gso', '{"brand":"萧康云医","summary":"诊所赋能SaaS平台","services":["患者端","医生端","老板端","推广员端","PC后台"]}', '默认GSO/GEO配置JSON', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + ('amap_key', '', '高德地图Key', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + ('amap_security_code', '', '高德地图安全密钥', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE `config_value` = VALUES(`config_value`), `updated_at` = UNIX_TIMESTAMP(); + +INSERT INTO `nl_menu` (`id`, `parent_id`, `title`, `name`, `path`, `component`, `icon`, `type`, `permission`, `visible`, `menu_area`, `status`, `sort`, `created_at`, `updated_at`) +VALUES + (23, 2, '图片管理', 'SiteMedia', '/site/media', 'SiteMedia', 'image', 2, 'site:media:view', 1, 2, 1, 3, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (24, 2, '招聘管理', 'RecruitJobs', '/site/jobs', 'RecruitJobs', 'briefcase', 2, 'site:job:view', 1, 2, 1, 4, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + (25, 2, '站点设置', 'SiteSettings', '/site/settings', 'SiteSettings', 'setting', 2, 'site:settings:view', 1, 2, 1, 5, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `updated_at` = UNIX_TIMESTAMP(); diff --git a/resource/public/html/.gitkeep b/resource/public/html/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/public/plugin/.gitkeep b/resource/public/plugin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/public/resource/css/.gitkeep b/resource/public/resource/css/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/public/resource/image/.gitkeep b/resource/public/resource/image/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/public/resource/js/.gitkeep b/resource/public/resource/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/template/.gitkeep b/resource/template/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/utility/.gitkeep b/utility/.gitkeep new file mode 100644 index 0000000..e69de29