初始化

This commit is contained in:
2026-07-18 05:58:41 +08:00
commit e4b95eb05e
69 changed files with 2289 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
---
description: 萧康云医项目全局编码规则
alwaysApply: true
---
# 全局规则
- 代码方法结构体接口关键业务分支复杂判断关键配置均需要编写清晰的中文注释
- 注释要说明业务意图输入输出边界条件和注意事项避免只重复代码字面含义
- 模块职责要清晰避免跨层调用重复实现和无关逻辑混入
- 新增或修改功能时优先遵循项目已有目录结构命名方式封装习惯和错误处理方式
- 公共能力应沉淀为可复用模块或组件避免在多个页面控制器或服务中复制粘贴

View 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 组件组成
- 组件应保持职责单一状态事件属性和插槽设计清楚避免单个组件承担过多页面逻辑
- 表单列表筛选弹窗空状态加载态错误态成功态等常见状态要完整设计
- 视觉风格要克制现代可信适合医疗和诊所经营场景避免过度营销化装饰化或单一色系堆叠

View 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
View File

@@ -0,0 +1 @@
* linguist-language=GO

19
.gitignore vendored Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
# GoFrame Template For SingleRepo
Quick Start:
- https://goframe.org/quick

176
api/admin/v1/admin.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
},
}
)

View File

@@ -0,0 +1 @@
package consts

View File

@@ -0,0 +1,9 @@
package admin
// ControllerV1 是后台管理控制器。
type ControllerV1 struct{}
// NewV1 创建后台管理控制器实例。
func NewV1() *ControllerV1 {
return &ControllerV1{}
}

View 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
}

View File

@@ -0,0 +1,5 @@
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package hello

View 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{}
}

View 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
}

View File

@@ -0,0 +1,9 @@
package lead
// ControllerV1 是官网线索控制器。
type ControllerV1 struct{}
// NewV1 创建官网线索控制器实例。
func NewV1() *ControllerV1 {
return &ControllerV1{}
}

View 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
}

View File

@@ -0,0 +1,9 @@
package order
// ControllerV1 是订购订单控制器。
type ControllerV1 struct{}
// NewV1 创建订购订单控制器实例。
func NewV1() *ControllerV1 {
return &ControllerV1{}
}

View 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
}

View File

@@ -0,0 +1,9 @@
package payment
// ControllerV1 是支付回调控制器。
type ControllerV1 struct{}
// NewV1 创建支付回调控制器实例。
func NewV1() *ControllerV1 {
return &ControllerV1{}
}

View 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
}

View File

@@ -0,0 +1,9 @@
package pricing
// ControllerV1 是套餐定价控制器。
type ControllerV1 struct{}
// NewV1 创建套餐定价控制器实例。
func NewV1() *ControllerV1 {
return &ControllerV1{}
}

View 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
}

View File

@@ -0,0 +1,9 @@
package site
// ControllerV1 是官网内容控制器。
type ControllerV1 struct{}
// NewV1 创建官网内容控制器实例。
func NewV1() *ControllerV1 {
return &ControllerV1{}
}

View 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
View File

0
internal/model/.gitkeep Normal file
View File

View File

View File

183
internal/model/website.go Normal file
View 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"`
}

View 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 状态仍为 200code 用于表达业务错误。
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})
}

View File

386
internal/service/admin.go Normal file
View 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
View 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
View 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}
}

View 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
View 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
View 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())
}

View 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

View File

@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml

View 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

View 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

View File

@@ -0,0 +1,10 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: template-single
spec:
template:
spec:
containers:
- name : main
image: template-single:develop

View File

@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
- configmap.yaml
patchesStrategicMerge:
- deployment.yaml
namespace: default

View 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

View File

@@ -0,0 +1,8 @@
#!/bin/bash
# This shell is executed before docker build.

0
manifest/i18n/.gitkeep Normal file
View File

View File

View 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();

View File

View File

View File

View File

View File

View File

0
utility/.gitkeep Normal file
View File