初始化

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

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
}