初始化
This commit is contained in:
43
internal/cmd/cmd.go
Normal file
43
internal/cmd/cmd.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcmd"
|
||||
|
||||
"xk-of-api/internal/controller/admin"
|
||||
"xk-of-api/internal/controller/hello"
|
||||
"xk-of-api/internal/controller/lead"
|
||||
"xk-of-api/internal/controller/order"
|
||||
"xk-of-api/internal/controller/payment"
|
||||
"xk-of-api/internal/controller/pricing"
|
||||
"xk-of-api/internal/controller/site"
|
||||
)
|
||||
|
||||
var (
|
||||
// Main 是 HTTP 服务启动命令,集中完成中间件和控制器绑定。
|
||||
Main = gcmd.Command{
|
||||
Name: "main",
|
||||
Usage: "main",
|
||||
Brief: "start http server",
|
||||
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
|
||||
s := g.Server()
|
||||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(ghttp.MiddlewareCORS)
|
||||
group.Bind(
|
||||
hello.NewV1(),
|
||||
site.NewV1(),
|
||||
pricing.NewV1(),
|
||||
lead.NewV1(),
|
||||
order.NewV1(),
|
||||
payment.NewV1(),
|
||||
admin.NewV1(),
|
||||
)
|
||||
})
|
||||
s.Run()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
)
|
||||
1
internal/consts/consts.go
Normal file
1
internal/consts/consts.go
Normal file
@@ -0,0 +1 @@
|
||||
package consts
|
||||
9
internal/controller/admin/admin.go
Normal file
9
internal/controller/admin/admin.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package admin
|
||||
|
||||
// ControllerV1 是后台管理控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建后台管理控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
111
internal/controller/admin/admin_v1.go
Normal file
111
internal/controller/admin/admin_v1.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"xk-of-api/api/admin/v1"
|
||||
"xk-of-api/internal/model"
|
||||
"xk-of-api/internal/response"
|
||||
"xk-of-api/internal/service"
|
||||
)
|
||||
|
||||
// Login 处理后台登录请求。
|
||||
func (c *ControllerV1) Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) {
|
||||
response.Success(ctx, service.Admin.Login(req.Account))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// MenuTree 返回后台多级菜单树。
|
||||
func (c *ControllerV1) MenuTree(ctx context.Context, req *v1.MenuTreeReq) (res *v1.MenuTreeRes, err error) {
|
||||
response.Success(ctx, service.Admin.Menus())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// MenuLayout 返回当前管理员菜单布局。
|
||||
func (c *ControllerV1) MenuLayout(ctx context.Context, req *v1.MenuLayoutReq) (res *v1.MenuLayoutRes, err error) {
|
||||
response.Success(ctx, service.Admin.MenuLayout())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UpdateMenuLayout 更新当前管理员菜单布局偏好。
|
||||
func (c *ControllerV1) UpdateMenuLayout(ctx context.Context, req *v1.UpdateMenuLayoutReq) (res *v1.UpdateMenuLayoutRes, err error) {
|
||||
response.Success(ctx, service.Admin.UpdateMenuLayout(req.MenuLayout))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Roles 返回角色列表。
|
||||
func (c *ControllerV1) Roles(ctx context.Context, req *v1.RolesReq) (res *v1.RolesRes, err error) {
|
||||
response.Success(ctx, service.Admin.Roles())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveRoleMenus 保存角色菜单授权。
|
||||
func (c *ControllerV1) SaveRoleMenus(ctx context.Context, req *v1.SaveRoleMenusReq) (res *v1.SaveRoleMenusRes, err error) {
|
||||
response.Success(ctx, service.Admin.SaveRoleMenus(req.RoleId, req.MenuIds))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SitePages 获取页面 SEO/GSO 配置。
|
||||
func (c *ControllerV1) SitePages(ctx context.Context, req *v1.SitePagesReq) (res *v1.SitePagesRes, err error) {
|
||||
response.Success(ctx, service.Admin.SitePages())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveSitePage 保存页面 SEO/GSO 配置。
|
||||
func (c *ControllerV1) SaveSitePage(ctx context.Context, req *v1.SaveSitePageReq) (res *v1.SaveSitePageRes, err error) {
|
||||
page := model.SitePage{Id: req.Id, Code: req.Code, Title: req.Title, SeoTitle: req.SeoTitle, SeoKeywords: req.SeoKeywords, SeoDescription: req.SeoDescription, GsoContent: req.GsoContent, Status: req.Status, Sort: req.Sort}
|
||||
response.Success(ctx, service.Admin.SaveSitePage(page))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SiteSections 获取官网页面模块。
|
||||
func (c *ControllerV1) SiteSections(ctx context.Context, req *v1.SiteSectionsReq) (res *v1.SiteSectionsRes, err error) {
|
||||
response.Success(ctx, service.Admin.SiteSections(req.PageCode))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveSiteSection 保存官网页面模块。
|
||||
func (c *ControllerV1) SaveSiteSection(ctx context.Context, req *v1.SaveSiteSectionReq) (res *v1.SaveSiteSectionRes, err error) {
|
||||
section := model.SiteSection{Id: req.Id, PageCode: req.PageCode, SectionKey: req.SectionKey, Title: req.Title, Subtitle: req.Subtitle, Content: req.Content, Status: req.Status, Sort: req.Sort}
|
||||
response.Success(ctx, service.Admin.SaveSiteSection(section))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SiteMedia 获取官网素材列表。
|
||||
func (c *ControllerV1) SiteMedia(ctx context.Context, req *v1.SiteMediaReq) (res *v1.SiteMediaRes, err error) {
|
||||
response.Success(ctx, service.Admin.SiteMedia(req.MediaType))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveSiteMedia 保存官网素材。
|
||||
func (c *ControllerV1) SaveSiteMedia(ctx context.Context, req *v1.SaveSiteMediaReq) (res *v1.SaveSiteMediaRes, err error) {
|
||||
media := model.SiteMedia{Id: req.Id, MediaType: req.MediaType, GroupCode: req.GroupCode, Title: req.Title, Url: req.Url, Alt: req.Alt, Status: req.Status, Sort: req.Sort}
|
||||
response.Success(ctx, service.Admin.SaveSiteMedia(media))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// RecruitJobs 获取后台招聘职位列表。
|
||||
func (c *ControllerV1) RecruitJobs(ctx context.Context, req *v1.RecruitJobsReq) (res *v1.RecruitJobsRes, err error) {
|
||||
response.Success(ctx, service.Admin.RecruitJobs())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveRecruitJob 保存招聘职位。
|
||||
func (c *ControllerV1) SaveRecruitJob(ctx context.Context, req *v1.SaveRecruitJobReq) (res *v1.SaveRecruitJobRes, err error) {
|
||||
job := model.RecruitJob{Id: req.Id, Title: req.Title, Department: req.Department, JobType: req.JobType, City: req.City, Salary: req.Salary, Experience: req.Experience, Description: req.Description, Requirements: req.Requirements, Manager: req.Manager, ManagerTitle: req.ManagerTitle, Email: req.Email, Phone: req.Phone, Featured: req.Featured, Status: req.Status, Sort: req.Sort}
|
||||
response.Success(ctx, service.Admin.SaveRecruitJob(job))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SiteSettings 获取站点全局配置。
|
||||
func (c *ControllerV1) SiteSettings(ctx context.Context, req *v1.SiteSettingsReq) (res *v1.SiteSettingsRes, err error) {
|
||||
response.Success(ctx, service.Admin.SiteSettings())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveSiteSettings 保存站点全局配置。
|
||||
func (c *ControllerV1) SaveSiteSettings(ctx context.Context, req *v1.SaveSiteSettingsReq) (res *v1.SaveSiteSettingsRes, err error) {
|
||||
settings := model.SiteSettings{SiteTitle: req.SiteTitle, SiteLogo: req.SiteLogo, SiteLogoDark: req.SiteLogoDark, SiteFavicon: req.SiteFavicon, IcpText: req.IcpText, Copyright: req.Copyright, ContactPhone: req.ContactPhone, ContactEmail: req.ContactEmail, ContactAddress: req.ContactAddress, DefaultSeo: req.DefaultSeo, DefaultGso: req.DefaultGso, AmapKey: req.AmapKey, AmapSecurityCode: req.AmapSecurityCode}
|
||||
response.Success(ctx, service.Admin.SaveSiteSettings(settings))
|
||||
return nil, nil
|
||||
}
|
||||
5
internal/controller/hello/hello.go
Normal file
5
internal/controller/hello/hello.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package hello
|
||||
15
internal/controller/hello/hello_new.go
Normal file
15
internal/controller/hello/hello_new.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package hello
|
||||
|
||||
import (
|
||||
"xk-of-api/api/hello"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
|
||||
func NewV1() hello.IHelloV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
13
internal/controller/hello/hello_v1_hello.go
Normal file
13
internal/controller/hello/hello_v1_hello.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package hello
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"xk-of-api/api/hello/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error) {
|
||||
g.RequestFromCtx(ctx).Response.Writeln("Hello World!")
|
||||
return
|
||||
}
|
||||
9
internal/controller/lead/lead.go
Normal file
9
internal/controller/lead/lead.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package lead
|
||||
|
||||
// ControllerV1 是官网线索控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建官网线索控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
27
internal/controller/lead/lead_v1.go
Normal file
27
internal/controller/lead/lead_v1.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package lead
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"xk-of-api/api/lead/v1"
|
||||
"xk-of-api/internal/model"
|
||||
"xk-of-api/internal/response"
|
||||
"xk-of-api/internal/service"
|
||||
)
|
||||
|
||||
// Create 创建官网咨询或预约演示线索。
|
||||
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
|
||||
result, err := service.Lead.Create(model.Lead{Name: req.Name, Phone: req.Phone, ClinicName: req.ClinicName, City: req.City, PlanCode: req.PlanCode, SourcePage: req.SourcePage, Remark: req.Remark})
|
||||
if err != nil {
|
||||
response.Error(ctx, 400, err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
response.Success(ctx, result)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// AdminList 获取后台线索列表。
|
||||
func (c *ControllerV1) AdminList(ctx context.Context, req *v1.AdminListReq) (res *v1.AdminListRes, err error) {
|
||||
response.Success(ctx, service.Lead.List())
|
||||
return nil, nil
|
||||
}
|
||||
9
internal/controller/order/order.go
Normal file
9
internal/controller/order/order.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package order
|
||||
|
||||
// ControllerV1 是订购订单控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建订购订单控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
37
internal/controller/order/order_v1.go
Normal file
37
internal/controller/order/order_v1.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"xk-of-api/api/order/v1"
|
||||
"xk-of-api/internal/model"
|
||||
"xk-of-api/internal/response"
|
||||
"xk-of-api/internal/service"
|
||||
)
|
||||
|
||||
// Create 创建套餐订购订单。
|
||||
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
|
||||
result, err := service.Order.Create(model.Order{TenantName: req.TenantName, ContactName: req.ContactName, Phone: req.Phone, PlanCode: req.PlanCode, BillingCycle: req.BillingCycle})
|
||||
if err != nil {
|
||||
response.Error(ctx, 400, err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
response.Success(ctx, result)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Detail 获取订单详情。
|
||||
func (c *ControllerV1) Detail(ctx context.Context, req *v1.DetailReq) (res *v1.DetailRes, err error) {
|
||||
if req.OrderNo == "" {
|
||||
response.Error(ctx, 400, "订单号不能为空")
|
||||
return nil, nil
|
||||
}
|
||||
response.Success(ctx, service.Order.Detail(req.OrderNo))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// AdminList 获取后台订单列表。
|
||||
func (c *ControllerV1) AdminList(ctx context.Context, req *v1.AdminListReq) (res *v1.AdminListRes, err error) {
|
||||
response.Success(ctx, service.Order.List())
|
||||
return nil, nil
|
||||
}
|
||||
9
internal/controller/payment/payment.go
Normal file
9
internal/controller/payment/payment.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package payment
|
||||
|
||||
// ControllerV1 是支付回调控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建支付回调控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
21
internal/controller/payment/payment_v1.go
Normal file
21
internal/controller/payment/payment_v1.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"xk-of-api/api/payment/v1"
|
||||
"xk-of-api/internal/response"
|
||||
"xk-of-api/internal/service"
|
||||
)
|
||||
|
||||
// Notify 接收支付渠道回调,后续在 service 中扩展验签和状态流转。
|
||||
func (c *ControllerV1) Notify(ctx context.Context, req *v1.NotifyReq) (res *v1.NotifyRes, err error) {
|
||||
if req.Channel == "" {
|
||||
response.Error(ctx, 400, "支付渠道不能为空")
|
||||
return nil, nil
|
||||
}
|
||||
response.Success(ctx, service.Order.Notify(req.Channel, g.RequestFromCtx(ctx).GetMap()))
|
||||
return nil, nil
|
||||
}
|
||||
9
internal/controller/pricing/pricing.go
Normal file
9
internal/controller/pricing/pricing.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package pricing
|
||||
|
||||
// ControllerV1 是套餐定价控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建套餐定价控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
21
internal/controller/pricing/pricing_v1.go
Normal file
21
internal/controller/pricing/pricing_v1.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package pricing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"xk-of-api/api/pricing/v1"
|
||||
"xk-of-api/internal/response"
|
||||
"xk-of-api/internal/service"
|
||||
)
|
||||
|
||||
// Plans 获取官网可展示套餐。
|
||||
func (c *ControllerV1) Plans(ctx context.Context, req *v1.PlansReq) (res *v1.PlansRes, err error) {
|
||||
response.Success(ctx, service.Pricing.Plans())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// AdminPlans 获取后台套餐列表。
|
||||
func (c *ControllerV1) AdminPlans(ctx context.Context, req *v1.AdminPlansReq) (res *v1.AdminPlansRes, err error) {
|
||||
response.Success(ctx, service.Pricing.Plans())
|
||||
return nil, nil
|
||||
}
|
||||
9
internal/controller/site/site.go
Normal file
9
internal/controller/site/site.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package site
|
||||
|
||||
// ControllerV1 是官网内容控制器。
|
||||
type ControllerV1 struct{}
|
||||
|
||||
// NewV1 创建官网内容控制器实例。
|
||||
func NewV1() *ControllerV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
27
internal/controller/site/site_v1_home.go
Normal file
27
internal/controller/site/site_v1_home.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package site
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"xk-of-api/api/site/v1"
|
||||
"xk-of-api/internal/response"
|
||||
"xk-of-api/internal/service"
|
||||
)
|
||||
|
||||
// Home 获取官网首页聚合内容。
|
||||
func (c *ControllerV1) Home(ctx context.Context, req *v1.HomeReq) (res *v1.HomeRes, err error) {
|
||||
response.Success(ctx, service.Site.Home())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Settings 获取前台站点公开配置。
|
||||
func (c *ControllerV1) Settings(ctx context.Context, req *v1.SettingsReq) (res *v1.SettingsRes, err error) {
|
||||
response.Success(ctx, service.Site.Settings())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// RecruitJobs 获取前台招聘职位。
|
||||
func (c *ControllerV1) RecruitJobs(ctx context.Context, req *v1.RecruitJobsReq) (res *v1.RecruitJobsRes, err error) {
|
||||
response.Success(ctx, service.Site.RecruitJobs())
|
||||
return nil, nil
|
||||
}
|
||||
0
internal/dao/.gitkeep
Normal file
0
internal/dao/.gitkeep
Normal file
0
internal/model/.gitkeep
Normal file
0
internal/model/.gitkeep
Normal file
0
internal/model/do/.gitkeep
Normal file
0
internal/model/do/.gitkeep
Normal file
0
internal/model/entity/.gitkeep
Normal file
0
internal/model/entity/.gitkeep
Normal file
183
internal/model/website.go
Normal file
183
internal/model/website.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package model
|
||||
|
||||
// SiteHome 是官网首页聚合数据,供前端一次性渲染核心官网模块。
|
||||
type SiteHome struct {
|
||||
Hero HeroBlock `json:"hero"`
|
||||
Products []ProductBlock `json:"products"`
|
||||
Highlights []ValueBlock `json:"highlights"`
|
||||
Faqs []FaqBlock `json:"faqs"`
|
||||
}
|
||||
|
||||
// HeroBlock 描述官网主视觉文案和关键指标。
|
||||
type HeroBlock struct {
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
PrimaryCTA string `json:"primary_cta"`
|
||||
SecondaryCTA string `json:"secondary_cta"`
|
||||
Stats []StatBlock `json:"stats"`
|
||||
}
|
||||
|
||||
// StatBlock 是官网展示的关键业务指标。
|
||||
type StatBlock struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// ProductBlock 描述患者端、医生端、老板端、推广员端和 PC 后台。
|
||||
type ProductBlock struct {
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
Description string `json:"description"`
|
||||
Features []string `json:"features"`
|
||||
}
|
||||
|
||||
// ValueBlock 描述诊所 SaaS 的核心能力。
|
||||
type ValueBlock struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
// FaqBlock 描述官网常见问题。
|
||||
type FaqBlock struct {
|
||||
Question string `json:"question"`
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
|
||||
// Plan 是 SaaS 套餐展示与订购使用的数据模型。
|
||||
type Plan struct {
|
||||
Id int `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
MonthlyFee int `json:"monthly_fee"`
|
||||
YearlyFee int `json:"yearly_fee"`
|
||||
Recommended bool `json:"recommended"`
|
||||
Features []PlanFeature `json:"features"`
|
||||
}
|
||||
|
||||
// PlanFeature 是套餐权益明细。
|
||||
type PlanFeature struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Highlight bool `json:"highlight"`
|
||||
}
|
||||
|
||||
// Lead 是官网咨询或预约演示线索。
|
||||
type Lead struct {
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
ClinicName string `json:"clinic_name"`
|
||||
City string `json:"city"`
|
||||
PlanCode string `json:"plan_code"`
|
||||
SourcePage string `json:"source_page"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// Order 是套餐订购订单。
|
||||
type Order struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
TenantName string `json:"tenant_name"`
|
||||
ContactName string `json:"contact_name"`
|
||||
Phone string `json:"phone"`
|
||||
PlanCode string `json:"plan_code"`
|
||||
BillingCycle string `json:"billing_cycle"`
|
||||
Amount int `json:"amount"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// Menu 是后台多级菜单节点,支持单列和双列布局渲染。
|
||||
type Menu struct {
|
||||
Id int `json:"id"`
|
||||
ParentId int `json:"parent_id"`
|
||||
Title string `json:"title"`
|
||||
Path string `json:"path"`
|
||||
Component string `json:"component"`
|
||||
Icon string `json:"icon"`
|
||||
Type string `json:"type"`
|
||||
Permission string `json:"permission"`
|
||||
MenuArea int `json:"menu_area"`
|
||||
Children []Menu `json:"children"`
|
||||
}
|
||||
|
||||
// MenuLayout 是后台菜单布局配置。
|
||||
type MenuLayout struct {
|
||||
GlobalLayout string `json:"global_layout"`
|
||||
UserLayout string `json:"user_layout"`
|
||||
FinalLayout string `json:"final_layout"`
|
||||
}
|
||||
|
||||
// SitePage 是官网页面配置,包含 SEO 与 GSO/GEO 信息。
|
||||
type SitePage struct {
|
||||
Id int `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
SeoTitle string `json:"seo_title"`
|
||||
SeoKeywords string `json:"seo_keywords"`
|
||||
SeoDescription string `json:"seo_description"`
|
||||
GsoContent map[string]interface{} `json:"gso_content"`
|
||||
Status int `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// SiteSection 是官网页面模块配置,内容 JSON 承载各模块可编辑字段。
|
||||
type SiteSection struct {
|
||||
Id int `json:"id"`
|
||||
PageCode string `json:"page_code"`
|
||||
SectionKey string `json:"section_key"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Content map[string]interface{} `json:"content"`
|
||||
Status int `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// SiteMedia 是官网素材,覆盖 LOGO、favicon、页面图、分享图和招聘图。
|
||||
type SiteMedia struct {
|
||||
Id int `json:"id"`
|
||||
MediaType string `json:"media_type"`
|
||||
GroupCode string `json:"group_code"`
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
Alt string `json:"alt"`
|
||||
Status int `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// RecruitJob 是招聘职位信息。
|
||||
type RecruitJob struct {
|
||||
Id int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Department string `json:"department"`
|
||||
JobType string `json:"job_type"`
|
||||
City string `json:"city"`
|
||||
Salary string `json:"salary"`
|
||||
Experience string `json:"experience"`
|
||||
Description string `json:"description"`
|
||||
Requirements []string `json:"requirements"`
|
||||
Manager string `json:"manager"`
|
||||
ManagerTitle string `json:"manager_title"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Featured bool `json:"featured"`
|
||||
Status int `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
PublishedDate string `json:"published_date"`
|
||||
}
|
||||
|
||||
// SiteSettings 是站点全局配置,页面未配置时作为默认值。
|
||||
type SiteSettings struct {
|
||||
SiteTitle string `json:"site_title"`
|
||||
SiteLogo string `json:"site_logo"`
|
||||
SiteLogoDark string `json:"site_logo_dark"`
|
||||
SiteFavicon string `json:"site_favicon"`
|
||||
IcpText string `json:"icp_text"`
|
||||
Copyright string `json:"copyright"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
ContactAddress string `json:"contact_address"`
|
||||
DefaultSeo map[string]interface{} `json:"default_seo"`
|
||||
DefaultGso map[string]interface{} `json:"default_gso"`
|
||||
AmapKey string `json:"amap_key"`
|
||||
AmapSecurityCode string `json:"amap_security_code"`
|
||||
}
|
||||
38
internal/response/response.go
Normal file
38
internal/response/response.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// Payload 是所有接口统一返回结构。
|
||||
// HTTP 状态码统一保持 200,前端通过 code 判断业务成功或失败。
|
||||
type Payload struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Result interface{} `json:"result"`
|
||||
}
|
||||
|
||||
// write 写出统一 JSON 并立即结束请求,避免 GF 默认响应中间件二次包装。
|
||||
func write(ctx context.Context, payload Payload) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
r.Response.Status = 200
|
||||
r.Response.WriteJsonExit(payload)
|
||||
}
|
||||
|
||||
// Success 返回统一成功响应,result 字段承载业务数据。
|
||||
func Success(ctx context.Context, result interface{}) {
|
||||
write(ctx, Payload{Code: 0, Message: "ok", Result: result})
|
||||
}
|
||||
|
||||
// Error 返回统一失败响应,HTTP 状态仍为 200,code 用于表达业务错误。
|
||||
func Error(ctx context.Context, code int, message string) {
|
||||
if code == 0 {
|
||||
code = 500
|
||||
}
|
||||
if message == "" {
|
||||
message = "服务器开小差了,请稍后重试"
|
||||
}
|
||||
write(ctx, Payload{Code: code, Message: message, Result: nil})
|
||||
}
|
||||
0
internal/service/.gitkeep
Normal file
0
internal/service/.gitkeep
Normal file
386
internal/service/admin.go
Normal file
386
internal/service/admin.go
Normal file
@@ -0,0 +1,386 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"xk-of-api/internal/model"
|
||||
)
|
||||
|
||||
// AdminService 负责后台登录、菜单、权限和官网 CMS 管理等后台业务逻辑。
|
||||
type AdminService struct{}
|
||||
|
||||
// Admin 暴露后台服务单例,控制器只调用服务层,不直接处理业务细节。
|
||||
var Admin = AdminService{}
|
||||
|
||||
var adminStore = struct {
|
||||
sync.RWMutex
|
||||
settings model.SiteSettings
|
||||
pages []model.SitePage
|
||||
sections []model.SiteSection
|
||||
media []model.SiteMedia
|
||||
jobs []model.RecruitJob
|
||||
}{
|
||||
settings: defaultSiteSettings(),
|
||||
pages: defaultSitePages(),
|
||||
sections: defaultSiteSections(),
|
||||
media: defaultSiteMedia(),
|
||||
jobs: defaultRecruitJobs(),
|
||||
}
|
||||
|
||||
// Login 返回后台登录占位结果,后续接入 nl_admin 密码校验和 token 签发。
|
||||
func (s AdminService) Login(account string) map[string]interface{} {
|
||||
if account == "" {
|
||||
account = "admin"
|
||||
}
|
||||
return map[string]interface{}{"token": "dev-token", "user": map[string]interface{}{"name": account, "role": "super_admin"}}
|
||||
}
|
||||
|
||||
// Menus 返回支持单列和双列布局的后台多级菜单树。
|
||||
func (s AdminService) Menus() []model.Menu {
|
||||
return []model.Menu{
|
||||
{Id: 1, ParentId: 0, Title: "控制台", Path: "/dashboard", Component: "Dashboard", Icon: "dashboard", Type: "menu", Permission: "dashboard:view", MenuArea: 1},
|
||||
{Id: 2, ParentId: 0, Title: "官网运营", Path: "/site", Component: "Layout", Icon: "global", Type: "catalog", Permission: "site:view", MenuArea: 1, Children: []model.Menu{
|
||||
{Id: 21, ParentId: 2, Title: "页面模块", Path: "/site/sections", Component: "SiteSections", Icon: "blocks", Type: "menu", Permission: "site:section:view", MenuArea: 2},
|
||||
{Id: 22, ParentId: 2, Title: "咨询线索", Path: "/site/leads", Component: "SiteLeads", Icon: "message", Type: "menu", Permission: "site:lead:view", MenuArea: 2},
|
||||
{Id: 23, ParentId: 2, Title: "图片管理", Path: "/site/media", Component: "SiteMedia", Icon: "image", Type: "menu", Permission: "site:media:view", MenuArea: 2},
|
||||
{Id: 24, ParentId: 2, Title: "招聘管理", Path: "/site/jobs", Component: "RecruitJobs", Icon: "briefcase", Type: "menu", Permission: "site:job:view", MenuArea: 2},
|
||||
{Id: 25, ParentId: 2, Title: "站点设置", Path: "/site/settings", Component: "SiteSettings", Icon: "setting", Type: "menu", Permission: "site:settings:view", MenuArea: 2},
|
||||
}},
|
||||
{Id: 3, ParentId: 0, Title: "商业化", Path: "/commerce", Component: "Layout", Icon: "wallet", Type: "catalog", Permission: "commerce:view", MenuArea: 1, Children: []model.Menu{
|
||||
{Id: 31, ParentId: 3, Title: "套餐管理", Path: "/commerce/plans", Component: "PlanList", Icon: "package", Type: "menu", Permission: "plan:view", MenuArea: 2},
|
||||
{Id: 32, ParentId: 3, Title: "订单管理", Path: "/commerce/orders", Component: "OrderList", Icon: "orders", Type: "menu", Permission: "order:view", MenuArea: 2},
|
||||
}},
|
||||
{Id: 4, ParentId: 0, Title: "系统管理", Path: "/system", Component: "Layout", Icon: "setting", Type: "catalog", Permission: "system:view", MenuArea: 1, Children: []model.Menu{
|
||||
{Id: 41, ParentId: 4, Title: "管理员", Path: "/system/admins", Component: "AdminList", Icon: "user", Type: "menu", Permission: "admin:view", MenuArea: 2},
|
||||
{Id: 42, ParentId: 4, Title: "角色权限", Path: "/system/roles", Component: "RoleList", Icon: "safety", Type: "menu", Permission: "role:view", MenuArea: 2},
|
||||
{Id: 43, ParentId: 4, Title: "菜单管理", Path: "/system/menus", Component: "MenuList", Icon: "menu", Type: "menu", Permission: "menu:view", MenuArea: 2},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// MenuLayout 返回全局默认布局和用户最终布局。
|
||||
func (s AdminService) MenuLayout() model.MenuLayout {
|
||||
return model.MenuLayout{GlobalLayout: "double", UserLayout: "", FinalLayout: "double"}
|
||||
}
|
||||
|
||||
// UpdateMenuLayout 保存管理员个人菜单布局偏好。
|
||||
func (s AdminService) UpdateMenuLayout(layout string) model.MenuLayout {
|
||||
if layout != "single" && layout != "double" {
|
||||
layout = "double"
|
||||
}
|
||||
return model.MenuLayout{GlobalLayout: "double", UserLayout: layout, FinalLayout: layout}
|
||||
}
|
||||
|
||||
// Roles 返回后台角色占位数据。
|
||||
func (s AdminService) Roles() []map[string]interface{} {
|
||||
return []map[string]interface{}{{"id": 1, "name": "超级管理员", "code": "super_admin"}, {"id": 2, "name": "运营人员", "code": "operator"}}
|
||||
}
|
||||
|
||||
// SaveRoleMenus 保存角色菜单授权占位结果。
|
||||
func (s AdminService) SaveRoleMenus(roleId int, menuIds []int) map[string]interface{} {
|
||||
return map[string]interface{}{"role_id": roleId, "menu_ids": menuIds, "saved": true}
|
||||
}
|
||||
|
||||
// SitePages 返回官网页面配置,支持每个页面独立维护 SEO 和 GSO/GEO 内容。
|
||||
func (s AdminService) SitePages() []model.SitePage {
|
||||
adminStore.RLock()
|
||||
defer adminStore.RUnlock()
|
||||
return copyPages(adminStore.pages)
|
||||
}
|
||||
|
||||
// SaveSitePage 保存页面 SEO/GSO 配置;一期先写入内存,后续替换为 nl_site_page 持久化。
|
||||
func (s AdminService) SaveSitePage(page model.SitePage) model.SitePage {
|
||||
adminStore.Lock()
|
||||
defer adminStore.Unlock()
|
||||
if page.Id == 0 {
|
||||
page.Id = nextPageId(adminStore.pages)
|
||||
}
|
||||
if page.GsoContent == nil {
|
||||
page.GsoContent = map[string]interface{}{}
|
||||
}
|
||||
adminStore.pages = upsertPage(adminStore.pages, page)
|
||||
return page
|
||||
}
|
||||
|
||||
// SiteSections 返回官网页面模块配置,可按页面编码过滤。
|
||||
func (s AdminService) SiteSections(pageCode string) []model.SiteSection {
|
||||
adminStore.RLock()
|
||||
defer adminStore.RUnlock()
|
||||
sections := make([]model.SiteSection, 0)
|
||||
for _, section := range adminStore.sections {
|
||||
if pageCode == "" || section.PageCode == pageCode {
|
||||
sections = append(sections, section)
|
||||
}
|
||||
}
|
||||
sortSections(sections)
|
||||
return sections
|
||||
}
|
||||
|
||||
// SaveSiteSection 保存官网页面模块;内容字段保持 JSON 结构,便于后台控制标题、按钮、列表和图片。
|
||||
func (s AdminService) SaveSiteSection(section model.SiteSection) model.SiteSection {
|
||||
adminStore.Lock()
|
||||
defer adminStore.Unlock()
|
||||
if section.Id == 0 {
|
||||
section.Id = nextSectionId(adminStore.sections)
|
||||
}
|
||||
if section.Content == nil {
|
||||
section.Content = map[string]interface{}{}
|
||||
}
|
||||
adminStore.sections = upsertSection(adminStore.sections, section)
|
||||
return section
|
||||
}
|
||||
|
||||
// SiteMedia 返回官网素材,覆盖 LOGO、favicon、页面图片、分享图和招聘图。
|
||||
func (s AdminService) SiteMedia(mediaType string) []model.SiteMedia {
|
||||
adminStore.RLock()
|
||||
defer adminStore.RUnlock()
|
||||
items := make([]model.SiteMedia, 0)
|
||||
for _, item := range adminStore.media {
|
||||
if mediaType == "" || item.MediaType == mediaType {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
sortMedia(items)
|
||||
return items
|
||||
}
|
||||
|
||||
// SaveSiteMedia 保存官网素材 URL;真实上传和对象存储可在后续扩展。
|
||||
func (s AdminService) SaveSiteMedia(media model.SiteMedia) model.SiteMedia {
|
||||
adminStore.Lock()
|
||||
defer adminStore.Unlock()
|
||||
if media.Id == 0 {
|
||||
media.Id = nextMediaId(adminStore.media)
|
||||
}
|
||||
adminStore.media = upsertMedia(adminStore.media, media)
|
||||
return media
|
||||
}
|
||||
|
||||
// RecruitJobs 返回后台招聘职位列表。
|
||||
func (s AdminService) RecruitJobs() []model.RecruitJob {
|
||||
adminStore.RLock()
|
||||
defer adminStore.RUnlock()
|
||||
jobs := append([]model.RecruitJob(nil), adminStore.jobs...)
|
||||
sortJobs(jobs)
|
||||
return jobs
|
||||
}
|
||||
|
||||
// SaveRecruitJob 保存招聘职位;一期先写入内存,后续接入 nl_recruit_job。
|
||||
func (s AdminService) SaveRecruitJob(job model.RecruitJob) model.RecruitJob {
|
||||
adminStore.Lock()
|
||||
defer adminStore.Unlock()
|
||||
if job.Id == 0 {
|
||||
job.Id = nextJobId(adminStore.jobs)
|
||||
}
|
||||
if job.Requirements == nil {
|
||||
job.Requirements = []string{}
|
||||
}
|
||||
if job.PublishedDate == "" {
|
||||
job.PublishedDate = "2026-07-05"
|
||||
}
|
||||
adminStore.jobs = upsertJob(adminStore.jobs, job)
|
||||
return job
|
||||
}
|
||||
|
||||
// SiteSettings 返回站点全局配置,页面未配置时使用这里的默认值。
|
||||
func (s AdminService) SiteSettings() model.SiteSettings {
|
||||
adminStore.RLock()
|
||||
defer adminStore.RUnlock()
|
||||
return copySettings(adminStore.settings)
|
||||
}
|
||||
|
||||
// SaveSiteSettings 保存站点全局配置,包含标题、LOGO、ico、SEO、GSO/GEO 和高德地图配置。
|
||||
func (s AdminService) SaveSiteSettings(settings model.SiteSettings) model.SiteSettings {
|
||||
adminStore.Lock()
|
||||
defer adminStore.Unlock()
|
||||
if settings.DefaultSeo == nil {
|
||||
settings.DefaultSeo = map[string]interface{}{}
|
||||
}
|
||||
if settings.DefaultGso == nil {
|
||||
settings.DefaultGso = map[string]interface{}{}
|
||||
}
|
||||
adminStore.settings = copySettings(settings)
|
||||
return copySettings(adminStore.settings)
|
||||
}
|
||||
|
||||
func defaultSiteSettings() model.SiteSettings {
|
||||
return model.SiteSettings{
|
||||
SiteTitle: "萧康云医官网",
|
||||
SiteLogo: "/src/assets/images/logo.png",
|
||||
SiteLogoDark: "/src/assets/images/logo-white.png",
|
||||
SiteFavicon: "/favicon.ico",
|
||||
IcpText: "浙ICP备0000000号",
|
||||
Copyright: "Copyright © 萧康云医",
|
||||
ContactPhone: "0571-00000000",
|
||||
ContactEmail: "contact@xiaokang.example",
|
||||
ContactAddress: "杭州市萧山区鸿盛路与高新六路交叉口东200米",
|
||||
DefaultSeo: map[string]interface{}{
|
||||
"title": "萧康云医官网",
|
||||
"keywords": "诊所SaaS,云医,诊所管理",
|
||||
"description": "萧康云医为诊所提供获客、预约、接诊、复诊和经营分析能力。",
|
||||
},
|
||||
DefaultGso: map[string]interface{}{
|
||||
"brand": "萧康云医",
|
||||
"summary": "面向诊所赋能的 SaaS 平台",
|
||||
"services": []string{"患者端", "医生端", "老板端", "推广员端", "PC后台"},
|
||||
},
|
||||
AmapKey: "",
|
||||
AmapSecurityCode: "",
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSitePages() []model.SitePage {
|
||||
return []model.SitePage{
|
||||
{Id: 1, Code: "home", Title: "首页", SeoTitle: "萧康云医官网", SeoKeywords: "诊所SaaS,诊所管理,云医", SeoDescription: "萧康云医为诊所提供获客、预约、接诊、复诊和经营分析能力。", GsoContent: map[string]interface{}{"summary": "面向诊所赋能的 SaaS 平台", "entities": []string{"萧康云医", "诊所SaaS"}}, Status: 1, Sort: 1},
|
||||
{Id: 2, Code: "features", Title: "产品能力", SeoTitle: "萧康云医产品能力", SeoKeywords: "患者端,医生端,推广员端,PC后台", SeoDescription: "了解萧康云医患者端、医生端、老板端、推广员端和后台能力。", GsoContent: map[string]interface{}{"summary": "多端协同产品能力"}, Status: 1, Sort: 2},
|
||||
{Id: 3, Code: "pricing", Title: "价格方案", SeoTitle: "萧康云医价格", SeoKeywords: "SaaS定价,诊所套餐", SeoDescription: "查看萧康云医基础版、增长版、连锁版套餐。", GsoContent: map[string]interface{}{"summary": "诊所 SaaS 套餐价格"}, Status: 1, Sort: 3},
|
||||
{Id: 4, Code: "careers", Title: "加入我们", SeoTitle: "加入萧康云医", SeoKeywords: "招聘,医疗SaaS招聘", SeoDescription: "加入萧康云医团队,共建诊所赋能平台。", GsoContent: map[string]interface{}{"summary": "萧康云医招聘职位"}, Status: 1, Sort: 4},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSiteSections() []model.SiteSection {
|
||||
return []model.SiteSection{
|
||||
{Id: 1, PageCode: "features", SectionKey: "hero", Title: "能力地图", Subtitle: "多端协同与经营后台", Content: map[string]interface{}{"layout": "capability_map", "image": "/src/assets/images/features0001.jpeg"}, Status: 1, Sort: 1},
|
||||
{Id: 2, PageCode: "features", SectionKey: "workflow", Title: "流程切片", Subtitle: "获客到复诊", Content: map[string]interface{}{"steps": []string{"获客", "预约", "接诊", "复诊", "复盘"}}, Status: 1, Sort: 2},
|
||||
{Id: 3, PageCode: "pricing", SectionKey: "plans", Title: "套餐矩阵", Subtitle: "月付/年付与权益对比", Content: map[string]interface{}{"billing": []string{"month", "year"}}, Status: 1, Sort: 1},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSiteMedia() []model.SiteMedia {
|
||||
return []model.SiteMedia{
|
||||
{Id: 1, MediaType: "logo", GroupCode: "brand", Title: "站点LOGO", Url: "/src/assets/images/logo.png", Alt: "萧康云医LOGO", Status: 1, Sort: 1},
|
||||
{Id: 2, MediaType: "favicon", GroupCode: "brand", Title: "favicon", Url: "/favicon.ico", Alt: "萧康云医图标", Status: 1, Sort: 2},
|
||||
{Id: 3, MediaType: "page_image", GroupCode: "features", Title: "产品能力首图", Url: "/src/assets/images/features0001.jpeg", Alt: "萧康云医产品能力", Status: 1, Sort: 3},
|
||||
{Id: 4, MediaType: "share_image", GroupCode: "seo", Title: "默认分享图", Url: "/src/assets/images/home0001.jpeg", Alt: "萧康云医官网分享图", Status: 1, Sort: 4},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultRecruitJobs() []model.RecruitJob {
|
||||
return []model.RecruitJob{
|
||||
{Id: 1, Title: "高级前端开发工程师", Department: "技术部", JobType: "全职", City: "杭州", Salary: "25K-45K", Experience: "5年以上", Description: "负责萧康云医官网、后台和小程序端前端架构与体验优化。", Requirements: []string{"熟悉 Vue 3 与工程化", "有中后台或 SaaS 项目经验", "关注性能、可访问性和组件化"}, Manager: "张经理", ManagerTitle: "技术负责人", Email: "tech@xiaokang.example", Phone: "0571-00000000", Featured: true, Status: 1, Sort: 1, PublishedDate: "2026-07-05"},
|
||||
{Id: 2, Title: "诊所运营顾问", Department: "运营部", JobType: "全职", City: "杭州", Salary: "12K-22K", Experience: "3年以上", Description: "面向诊所客户完成系统演示、上线辅导和经营复盘。", Requirements: []string{"熟悉医疗或本地生活服务行业", "具备客户培训和项目推进能力", "表达清晰,能沉淀方法论"}, Manager: "王经理", ManagerTitle: "运营负责人", Email: "ops@xiaokang.example", Phone: "0571-00000001", Featured: false, Status: 1, Sort: 2, PublishedDate: "2026-07-05"},
|
||||
}
|
||||
}
|
||||
|
||||
func copyMap(source map[string]interface{}) map[string]interface{} {
|
||||
if source == nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
result := make(map[string]interface{}, len(source))
|
||||
for key, value := range source {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func copySettings(settings model.SiteSettings) model.SiteSettings {
|
||||
settings.DefaultSeo = copyMap(settings.DefaultSeo)
|
||||
settings.DefaultGso = copyMap(settings.DefaultGso)
|
||||
return settings
|
||||
}
|
||||
|
||||
func copyPages(pages []model.SitePage) []model.SitePage {
|
||||
items := append([]model.SitePage(nil), pages...)
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return items[i].Sort < items[j].Sort || items[i].Sort == items[j].Sort && items[i].Id < items[j].Id
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
func upsertPage(items []model.SitePage, item model.SitePage) []model.SitePage {
|
||||
for index := range items {
|
||||
if items[index].Id == item.Id || item.Code != "" && items[index].Code == item.Code {
|
||||
items[index] = item
|
||||
return items
|
||||
}
|
||||
}
|
||||
return append(items, item)
|
||||
}
|
||||
|
||||
func upsertSection(items []model.SiteSection, item model.SiteSection) []model.SiteSection {
|
||||
for index := range items {
|
||||
if items[index].Id == item.Id || item.SectionKey != "" && items[index].PageCode == item.PageCode && items[index].SectionKey == item.SectionKey {
|
||||
items[index] = item
|
||||
return items
|
||||
}
|
||||
}
|
||||
return append(items, item)
|
||||
}
|
||||
|
||||
func upsertMedia(items []model.SiteMedia, item model.SiteMedia) []model.SiteMedia {
|
||||
for index := range items {
|
||||
if items[index].Id == item.Id {
|
||||
items[index] = item
|
||||
return items
|
||||
}
|
||||
}
|
||||
return append(items, item)
|
||||
}
|
||||
|
||||
func upsertJob(items []model.RecruitJob, item model.RecruitJob) []model.RecruitJob {
|
||||
for index := range items {
|
||||
if items[index].Id == item.Id {
|
||||
items[index] = item
|
||||
return items
|
||||
}
|
||||
}
|
||||
return append(items, item)
|
||||
}
|
||||
|
||||
func sortSections(items []model.SiteSection) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return items[i].Sort < items[j].Sort || items[i].Sort == items[j].Sort && items[i].Id < items[j].Id
|
||||
})
|
||||
}
|
||||
|
||||
func sortMedia(items []model.SiteMedia) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return items[i].Sort < items[j].Sort || items[i].Sort == items[j].Sort && items[i].Id < items[j].Id
|
||||
})
|
||||
}
|
||||
|
||||
func sortJobs(items []model.RecruitJob) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return items[i].Sort < items[j].Sort || items[i].Sort == items[j].Sort && items[i].Id < items[j].Id
|
||||
})
|
||||
}
|
||||
|
||||
func nextPageId(items []model.SitePage) int {
|
||||
maxId := 0
|
||||
for _, item := range items {
|
||||
if item.Id > maxId {
|
||||
maxId = item.Id
|
||||
}
|
||||
}
|
||||
return maxId + 1
|
||||
}
|
||||
|
||||
func nextSectionId(items []model.SiteSection) int {
|
||||
maxId := 0
|
||||
for _, item := range items {
|
||||
if item.Id > maxId {
|
||||
maxId = item.Id
|
||||
}
|
||||
}
|
||||
return maxId + 1
|
||||
}
|
||||
|
||||
func nextMediaId(items []model.SiteMedia) int {
|
||||
maxId := 0
|
||||
for _, item := range items {
|
||||
if item.Id > maxId {
|
||||
maxId = item.Id
|
||||
}
|
||||
}
|
||||
return maxId + 1
|
||||
}
|
||||
|
||||
func nextJobId(items []model.RecruitJob) int {
|
||||
maxId := 0
|
||||
for _, item := range items {
|
||||
if item.Id > maxId {
|
||||
maxId = item.Id
|
||||
}
|
||||
}
|
||||
return maxId + 1
|
||||
}
|
||||
33
internal/service/lead.go
Normal file
33
internal/service/lead.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"xk-of-api/internal/model"
|
||||
)
|
||||
|
||||
// LeadService 负责官网线索校验和保存,后续接入 nl_site_lead。
|
||||
type LeadService struct{}
|
||||
|
||||
// Lead 暴露线索服务单例。
|
||||
var Lead = LeadService{}
|
||||
|
||||
// Create 校验并创建咨询线索。
|
||||
func (s LeadService) Create(lead model.Lead) (model.Lead, error) {
|
||||
if strings.TrimSpace(lead.Name) == "" {
|
||||
return model.Lead{}, errors.New("请填写联系人姓名")
|
||||
}
|
||||
if strings.TrimSpace(lead.Phone) == "" {
|
||||
return model.Lead{}, errors.New("请填写联系电话")
|
||||
}
|
||||
if lead.SourcePage == "" {
|
||||
lead.SourcePage = "website"
|
||||
}
|
||||
return lead, nil
|
||||
}
|
||||
|
||||
// List 返回后台线索列表占位数据。
|
||||
func (s LeadService) List() []model.Lead {
|
||||
return []model.Lead{{Name: "王医生", Phone: "13800000000", ClinicName: "萧康示例诊所", City: "上海", PlanCode: "growth", SourcePage: "pricing", Remark: "希望了解增长版"}}
|
||||
}
|
||||
50
internal/service/order.go
Normal file
50
internal/service/order.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"xk-of-api/internal/model"
|
||||
)
|
||||
|
||||
// OrderService 负责套餐订购和订单状态查询。
|
||||
type OrderService struct{}
|
||||
|
||||
// Order 暴露订单服务单例。
|
||||
var Order = OrderService{}
|
||||
|
||||
// Create 创建套餐订单;金额根据套餐和周期计算。
|
||||
func (s OrderService) Create(order model.Order) (model.Order, error) {
|
||||
if order.TenantName == "" || order.ContactName == "" || order.Phone == "" {
|
||||
return model.Order{}, errors.New("请补全诊所名称、联系人和手机号")
|
||||
}
|
||||
plan, ok := Pricing.FindByCode(order.PlanCode)
|
||||
if !ok {
|
||||
return model.Order{}, errors.New("套餐不存在")
|
||||
}
|
||||
if order.BillingCycle == "year" {
|
||||
order.Amount = plan.YearlyFee
|
||||
} else {
|
||||
order.BillingCycle = "month"
|
||||
order.Amount = plan.MonthlyFee
|
||||
}
|
||||
order.OrderNo = fmt.Sprintf("XK%d", time.Now().UnixNano())
|
||||
order.Status = "pending"
|
||||
return order, nil
|
||||
}
|
||||
|
||||
// Detail 返回订单详情占位数据,后续改为读取 nl_order。
|
||||
func (s OrderService) Detail(orderNo string) model.Order {
|
||||
return model.Order{OrderNo: orderNo, TenantName: "萧康示例诊所", ContactName: "王医生", Phone: "13800000000", PlanCode: "growth", BillingCycle: "year", Amount: 399000, Status: "pending"}
|
||||
}
|
||||
|
||||
// List 返回后台订单列表占位数据。
|
||||
func (s OrderService) List() []model.Order {
|
||||
return []model.Order{s.Detail("XK202607040001")}
|
||||
}
|
||||
|
||||
// Notify 处理支付渠道回调占位逻辑,真实渠道验签在后续适配器完成。
|
||||
func (s OrderService) Notify(channel string, payload map[string]interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{"channel": channel, "notify_status": "received", "payload": payload}
|
||||
}
|
||||
28
internal/service/pricing.go
Normal file
28
internal/service/pricing.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package service
|
||||
|
||||
import "xk-of-api/internal/model"
|
||||
|
||||
// PricingService 负责 SaaS 套餐展示和套餐查询。
|
||||
type PricingService struct{}
|
||||
|
||||
// Pricing 暴露定价服务单例。
|
||||
var Pricing = PricingService{}
|
||||
|
||||
// Plans 返回官网定价套餐,后续可由 nl_saas_plan 和 nl_saas_plan_feature 驱动。
|
||||
func (s PricingService) Plans() []model.Plan {
|
||||
return []model.Plan{
|
||||
{Id: 1, Code: "starter", Name: "基础版", Description: "适合单店诊所快速上线线上预约和基础经营管理。", MonthlyFee: 19900, YearlyFee: 199000, Recommended: false, Features: []model.PlanFeature{{Name: "患者端小程序", Value: "基础预约与提醒", Highlight: true}, {Name: "PC后台", Value: "线索与订单管理", Highlight: true}, {Name: "账号数量", Value: "5个员工账号"}}},
|
||||
{Id: 2, Code: "growth", Name: "增长版", Description: "适合需要推广员获客、医生协同和经营分析的成长型诊所。", MonthlyFee: 39900, YearlyFee: 399000, Recommended: true, Features: []model.PlanFeature{{Name: "四端小程序", Value: "患者/医生/老板/推广员", Highlight: true}, {Name: "经营看板", Value: "收入、转化、复购分析", Highlight: true}, {Name: "账号数量", Value: "20个员工账号"}}},
|
||||
{Id: 3, Code: "enterprise", Name: "连锁版", Description: "适合连锁门店和需要深度配置的诊所集团。", MonthlyFee: 89900, YearlyFee: 899000, Recommended: false, Features: []model.PlanFeature{{Name: "多门店能力", Value: "预留扩展与独立配置", Highlight: true}, {Name: "专属服务", Value: "部署辅导和运营顾问", Highlight: true}, {Name: "账号数量", Value: "不限账号数量"}}},
|
||||
}
|
||||
}
|
||||
|
||||
// FindByCode 根据套餐编码查找套餐。
|
||||
func (s PricingService) FindByCode(code string) (model.Plan, bool) {
|
||||
for _, plan := range s.Plans() {
|
||||
if plan.Code == code {
|
||||
return plan, true
|
||||
}
|
||||
}
|
||||
return model.Plan{}, false
|
||||
}
|
||||
59
internal/service/site.go
Normal file
59
internal/service/site.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package service
|
||||
|
||||
import "xk-of-api/internal/model"
|
||||
|
||||
// SiteService 负责官网公开内容聚合,后续可替换为数据库读取 nl_site_page/nl_site_section。
|
||||
type SiteService struct{}
|
||||
|
||||
// Site 暴露官网服务单例,控制器只调用服务层。
|
||||
var Site = SiteService{}
|
||||
|
||||
// Home 返回萧康云医官网首页聚合数据。
|
||||
func (s SiteService) Home() model.SiteHome {
|
||||
return model.SiteHome{
|
||||
Hero: model.HeroBlock{
|
||||
Title: "萧康云医,让诊所经营更清楚、更稳定、更可增长",
|
||||
Subtitle: "连接患者、医生、门店老板、推广员和 PC 管理后台,用一套 SaaS 平台完成获客、预约、接诊、复诊、经营分析和团队协同。",
|
||||
PrimaryCTA: "预约演示",
|
||||
SecondaryCTA: "查看定价",
|
||||
Stats: []model.StatBlock{
|
||||
{Value: "4端", Label: "小程序角色覆盖"},
|
||||
{Value: "1套", Label: "PC 经营后台"},
|
||||
{Value: "7x24", Label: "线上服务触达"},
|
||||
},
|
||||
},
|
||||
Products: []model.ProductBlock{
|
||||
{Name: "患者端", Role: "患者", Description: "预约、复诊、健康档案和服务提醒统一承载。", Features: []string{"在线预约", "复诊提醒", "服务记录"}},
|
||||
{Name: "医生端", Role: "医生", Description: "帮助医生管理接诊流程、患者跟进和医嘱记录。", Features: []string{"患者管理", "接诊记录", "随访任务"}},
|
||||
{Name: "门店老板端", Role: "经营者", Description: "实时查看门店经营、转化、复购和团队执行情况。", Features: []string{"经营看板", "员工绩效", "收入分析"}},
|
||||
{Name: "推广员端", Role: "推广员", Description: "追踪推广码、线索归属和成交转化,降低获客管理成本。", Features: []string{"推广码", "线索归属", "佣金统计"}},
|
||||
{Name: "PC 后台", Role: "管理员", Description: "完成权限、菜单、套餐、订单、线索和基础配置管理。", Features: []string{"权限菜单", "套餐管理", "订单管理"}},
|
||||
},
|
||||
Highlights: []model.ValueBlock{
|
||||
{Title: "诊所全链路赋能", Description: "从获客、预约、接诊到复诊运营,减少系统割裂。", Icon: "fa-solid fa-route"},
|
||||
{Title: "多角色协同", Description: "不同角色使用独立端口,数据统一汇总到经营后台。", Icon: "fa-solid fa-users-gear"},
|
||||
{Title: "经营数据沉淀", Description: "沉淀线索、订单、患者服务和团队执行数据。", Icon: "fa-solid fa-chart-line"},
|
||||
},
|
||||
Faqs: []model.FaqBlock{
|
||||
{Question: "适合哪些诊所?", Answer: "适合希望提升线上获客、复诊运营和经营管理效率的综合诊所、专科诊所和连锁门店。"},
|
||||
{Question: "是否支持多门店?", Answer: "一期先保留租户主体,后续可扩展门店、员工和分账等完整经营表。"},
|
||||
{Question: "可以先预约演示吗?", Answer: "可以,官网线索会进入后台,运营人员可跟进套餐和部署方案。"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Settings 返回前台可公开使用的站点配置,包含标题、LOGO、SEO/GSO 和地图配置。
|
||||
func (s SiteService) Settings() model.SiteSettings {
|
||||
return Admin.SiteSettings()
|
||||
}
|
||||
|
||||
// RecruitJobs 返回前台启用状态的招聘职位。
|
||||
func (s SiteService) RecruitJobs() []model.RecruitJob {
|
||||
jobs := make([]model.RecruitJob, 0)
|
||||
for _, job := range Admin.RecruitJobs() {
|
||||
if job.Status == 1 {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
}
|
||||
return jobs
|
||||
}
|
||||
Reference in New Issue
Block a user