初始化
This commit is contained in:
12
.cursor/rules/00-global.mdc
Normal file
12
.cursor/rules/00-global.mdc
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
description: 萧康云医项目全局编码规则
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 全局规则
|
||||
|
||||
- 代码、包、方法、结构体、接口、关键业务分支、复杂判断、关键配置均需要编写清晰的中文注释。
|
||||
- 注释要说明业务意图、输入输出、边界条件和注意事项,避免只重复代码字面含义。
|
||||
- 模块职责要清晰,避免跨层调用、重复实现和无关逻辑混入。
|
||||
- 新增或修改功能时,优先遵循项目已有目录结构、命名方式、封装习惯和错误处理方式。
|
||||
- 公共能力应沉淀为可复用模块或组件,避免在多个页面、控制器或服务中复制粘贴。
|
||||
17
.cursor/rules/10-frontend.mdc
Normal file
17
.cursor/rules/10-frontend.mdc
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
description: 萧康云医官网前端设计与实现规则
|
||||
globs: xk-of-view/**/*,**/*.html,**/*.css,**/*.scss,**/*.less,**/*.vue,**/*.tsx,**/*.ts,**/*.jsx,**/*.js
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 前端规则
|
||||
|
||||
- 项目名称为“萧康云医官网”,定位为面向诊所赋能业务的 SaaS 平台。
|
||||
- 产品端形态包含小程序患者端、医生端、门店老板端、推广员端,以及 PC 端管理或官网相关页面。
|
||||
- 页面设计要体现 SaaS 平台的专业、清晰、高效,优先服务信息扫描、业务转化和多角色理解。
|
||||
- 必须注意多端适配,兼容移动端、平板端和桌面端,不允许出现文字溢出、布局重叠、关键按钮不可触达等问题。
|
||||
- 必须提供暗色模式适配,颜色、阴影、边框、图标、图表和状态提示都要在明暗主题下保持可读。
|
||||
- 采用模块化、组件化设计,页面由清晰的业务模块和可复用 UI 组件组成。
|
||||
- 组件应保持职责单一,状态、事件、属性和插槽设计清楚,避免单个组件承担过多页面逻辑。
|
||||
- 表单、列表、筛选、弹窗、空状态、加载态、错误态、成功态等常见状态要完整设计。
|
||||
- 视觉风格要克制、现代、可信,适合医疗和诊所经营场景;避免过度营销化、装饰化或单一色系堆叠。
|
||||
17
.cursor/rules/20-backend-goframe.mdc
Normal file
17
.cursor/rules/20-backend-goframe.mdc
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
description: 萧康云医后端 GoFrame GF 分层开发规则
|
||||
globs: **/*.go,api/**/*,internal/**/*,manifest/**/*,resource/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 后端规则
|
||||
|
||||
- 后端使用 GoFrame GF 框架,必须遵循 GF 的项目结构、路由注册、控制器、服务、模型、配置和错误处理规范。
|
||||
- 分层链路为:路由 -> 控制器 -> service 服务层 -> 模型层。
|
||||
- 路由层只负责请求路径、HTTP 方法、中间件和控制器绑定,不编写业务逻辑。
|
||||
- 控制器层负责获取请求信息、参数验证、用户信息解析、调用 service,并返回统一响应。
|
||||
- 控制器层不编写核心业务逻辑,不直接操作数据库,不绕过 service 调用模型。
|
||||
- service 层是业务逻辑层,负责业务流程编排、权限判断、事务控制、数据处理和外部服务调用。
|
||||
- 模型层只用于定义表结构、表关系、字段映射和数据承载,不编写业务逻辑。
|
||||
- 数据库访问、事务、缓存、队列、第三方接口等能力应按 GF 推荐方式封装,并放在合适的服务或基础设施模块中。
|
||||
- 参数校验、错误码、日志、上下文传递和用户身份解析要保持统一规范。
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* linguist-language=GO
|
||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -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
|
||||
44
AGENTS.md
Normal file
44
AGENTS.md
Normal file
@@ -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 或嵌套响应。
|
||||
7
Makefile
Normal file
7
Makefile
Normal file
@@ -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
|
||||
4
README.MD
Normal file
4
README.MD
Normal file
@@ -0,0 +1,4 @@
|
||||
# GoFrame Template For SingleRepo
|
||||
|
||||
Quick Start:
|
||||
- https://goframe.org/quick
|
||||
176
api/admin/v1/admin.go
Normal file
176
api/admin/v1/admin.go
Normal file
@@ -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{}
|
||||
15
api/hello/hello.go
Normal file
15
api/hello/hello.go
Normal file
@@ -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)
|
||||
}
|
||||
12
api/hello/v1/hello.go
Normal file
12
api/hello/v1/hello.go
Normal file
@@ -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"`
|
||||
}
|
||||
24
api/lead/v1/lead.go
Normal file
24
api/lead/v1/lead.go
Normal file
@@ -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{}
|
||||
30
api/order/v1/order.go
Normal file
30
api/order/v1/order.go
Normal file
@@ -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{}
|
||||
11
api/payment/v1/payment.go
Normal file
11
api/payment/v1/payment.go
Normal file
@@ -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{}
|
||||
17
api/pricing/v1/pricing.go
Normal file
17
api/pricing/v1/pricing.go
Normal file
@@ -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{}
|
||||
24
api/site/v1/site.go
Normal file
24
api/site/v1/site.go
Normal file
@@ -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{}
|
||||
35
go.mod
Normal file
35
go.mod
Normal file
@@ -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
|
||||
)
|
||||
81
go.sum
Normal file
81
go.sum
Normal file
@@ -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=
|
||||
13
hack/config.yaml
Normal file
13
hack/config.yaml
Normal file
@@ -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
|
||||
18
hack/hack-cli.mk
Normal file
18
hack/hack-cli.mk
Normal file
@@ -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;
|
||||
75
hack/hack.mk
Normal file
75
hack/hack.mk
Normal file
@@ -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
|
||||
43
internal/cmd/cmd.go
Normal file
43
internal/cmd/cmd.go
Normal file
@@ -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
|
||||
},
|
||||
}
|
||||
)
|
||||
1
internal/consts/consts.go
Normal file
1
internal/consts/consts.go
Normal file
@@ -0,0 +1 @@
|
||||
package consts
|
||||
9
internal/controller/admin/admin.go
Normal file
9
internal/controller/admin/admin.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package admin
|
||||
|
||||
// ControllerV1 是后台管理控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建后台管理控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
111
internal/controller/admin/admin_v1.go
Normal file
111
internal/controller/admin/admin_v1.go
Normal file
@@ -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
|
||||
}
|
||||
5
internal/controller/hello/hello.go
Normal file
5
internal/controller/hello/hello.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package hello
|
||||
15
internal/controller/hello/hello_new.go
Normal file
15
internal/controller/hello/hello_new.go
Normal file
@@ -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{}
|
||||
}
|
||||
13
internal/controller/hello/hello_v1_hello.go
Normal file
13
internal/controller/hello/hello_v1_hello.go
Normal file
@@ -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
|
||||
}
|
||||
9
internal/controller/lead/lead.go
Normal file
9
internal/controller/lead/lead.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package lead
|
||||
|
||||
// ControllerV1 是官网线索控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建官网线索控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
27
internal/controller/lead/lead_v1.go
Normal file
27
internal/controller/lead/lead_v1.go
Normal file
@@ -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
|
||||
}
|
||||
9
internal/controller/order/order.go
Normal file
9
internal/controller/order/order.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package order
|
||||
|
||||
// ControllerV1 是订购订单控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建订购订单控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
37
internal/controller/order/order_v1.go
Normal file
37
internal/controller/order/order_v1.go
Normal file
@@ -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
|
||||
}
|
||||
9
internal/controller/payment/payment.go
Normal file
9
internal/controller/payment/payment.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package payment
|
||||
|
||||
// ControllerV1 是支付回调控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建支付回调控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
21
internal/controller/payment/payment_v1.go
Normal file
21
internal/controller/payment/payment_v1.go
Normal file
@@ -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
|
||||
}
|
||||
9
internal/controller/pricing/pricing.go
Normal file
9
internal/controller/pricing/pricing.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package pricing
|
||||
|
||||
// ControllerV1 是套餐定价控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建套餐定价控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
21
internal/controller/pricing/pricing_v1.go
Normal file
21
internal/controller/pricing/pricing_v1.go
Normal file
@@ -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
|
||||
}
|
||||
9
internal/controller/site/site.go
Normal file
9
internal/controller/site/site.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package site
|
||||
|
||||
// ControllerV1 是官网内容控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建官网内容控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
27
internal/controller/site/site_v1_home.go
Normal file
27
internal/controller/site/site_v1_home.go
Normal file
@@ -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
|
||||
}
|
||||
0
internal/dao/.gitkeep
Normal file
0
internal/dao/.gitkeep
Normal file
0
internal/model/.gitkeep
Normal file
0
internal/model/.gitkeep
Normal file
0
internal/model/do/.gitkeep
Normal file
0
internal/model/do/.gitkeep
Normal file
0
internal/model/entity/.gitkeep
Normal file
0
internal/model/entity/.gitkeep
Normal file
183
internal/model/website.go
Normal file
183
internal/model/website.go
Normal file
@@ -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"`
|
||||
}
|
||||
38
internal/response/response.go
Normal file
38
internal/response/response.go
Normal file
@@ -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})
|
||||
}
|
||||
0
internal/service/.gitkeep
Normal file
0
internal/service/.gitkeep
Normal file
386
internal/service/admin.go
Normal file
386
internal/service/admin.go
Normal file
@@ -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
|
||||
}
|
||||
33
internal/service/lead.go
Normal file
33
internal/service/lead.go
Normal file
@@ -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: "希望了解增长版"}}
|
||||
}
|
||||
50
internal/service/order.go
Normal file
50
internal/service/order.go
Normal file
@@ -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}
|
||||
}
|
||||
28
internal/service/pricing.go
Normal file
28
internal/service/pricing.go
Normal file
@@ -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
|
||||
}
|
||||
59
internal/service/site.go
Normal file
59
internal/service/site.go
Normal file
@@ -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
|
||||
}
|
||||
11
main.go
Normal file
11
main.go
Normal file
@@ -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())
|
||||
}
|
||||
21
manifest/deploy/kustomize/base/deployment.yaml
Normal file
21
manifest/deploy/kustomize/base/deployment.yaml
Normal file
@@ -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
|
||||
|
||||
8
manifest/deploy/kustomize/base/kustomization.yaml
Normal file
8
manifest/deploy/kustomize/base/kustomization.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
|
||||
|
||||
|
||||
12
manifest/deploy/kustomize/base/service.yaml
Normal file
12
manifest/deploy/kustomize/base/service.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: template-single
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8000
|
||||
selector:
|
||||
app: template-single
|
||||
|
||||
14
manifest/deploy/kustomize/overlays/develop/configmap.yaml
Normal file
14
manifest/deploy/kustomize/overlays/develop/configmap.yaml
Normal file
@@ -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
|
||||
10
manifest/deploy/kustomize/overlays/develop/deployment.yaml
Normal file
10
manifest/deploy/kustomize/overlays/develop/deployment.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: template-single
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name : main
|
||||
image: template-single:develop
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
- configmap.yaml
|
||||
|
||||
patchesStrategicMerge:
|
||||
- deployment.yaml
|
||||
|
||||
namespace: default
|
||||
|
||||
|
||||
|
||||
16
manifest/docker/Dockerfile
Normal file
16
manifest/docker/Dockerfile
Normal file
@@ -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
|
||||
8
manifest/docker/docker.sh
Normal file
8
manifest/docker/docker.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This shell is executed before docker build.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
manifest/i18n/.gitkeep
Normal file
0
manifest/i18n/.gitkeep
Normal file
0
manifest/protobuf/.keep-if-necessary
Normal file
0
manifest/protobuf/.keep-if-necessary
Normal file
371
manifest/sql/xk_of_schema.sql
Normal file
371
manifest/sql/xk_of_schema.sql
Normal file
@@ -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();
|
||||
0
resource/public/html/.gitkeep
Normal file
0
resource/public/html/.gitkeep
Normal file
0
resource/public/plugin/.gitkeep
Normal file
0
resource/public/plugin/.gitkeep
Normal file
0
resource/public/resource/css/.gitkeep
Normal file
0
resource/public/resource/css/.gitkeep
Normal file
0
resource/public/resource/image/.gitkeep
Normal file
0
resource/public/resource/image/.gitkeep
Normal file
0
resource/public/resource/js/.gitkeep
Normal file
0
resource/public/resource/js/.gitkeep
Normal file
0
resource/template/.gitkeep
Normal file
0
resource/template/.gitkeep
Normal file
0
utility/.gitkeep
Normal file
0
utility/.gitkeep
Normal file
Reference in New Issue
Block a user