chore: seal Design Vision v1 and monorepo scaffold
Archive the differentiated YuXinGu product docs, AI engineering system, design contract, and Go/Vue scaffold. Next execution prioritizes Cece-parity over early innovation (see .ai/product/STRATEGY.md). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# admin-h5
|
||||
|
||||
运营后台(内容库、订单、会员)。**业务后置**,当前仅占位。
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@yuxingu/admin-h5",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "运营后台占位,业务后置"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Command server starts the 愈心谷 HTTP API.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/config"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/handler"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
if cfg.AppEnv == "prod" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery(), gin.Logger(), middleware.RequestID())
|
||||
|
||||
api := r.Group("/api/v1")
|
||||
handler.NewHealthHandler().Register(api)
|
||||
|
||||
// Placeholder route to demonstrate unified envelope.
|
||||
api.GET("/ping", func(c *gin.Context) {
|
||||
response.OK(c, gin.H{"pong": true})
|
||||
})
|
||||
|
||||
addr := cfg.HTTPAddr
|
||||
log.Printf("yuxingu api listening on %s env=%s", addr, cfg.AppEnv)
|
||||
if err := r.Run(addr); err != nil {
|
||||
log.Printf("server stopped: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
module github.com/yuxingu/digital-psychology/apps/api
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/gin-gonic/gin v1.10.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
@@ -0,0 +1,27 @@
|
||||
// Package config loads process configuration from environment variables.
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
// Config holds runtime settings for the API server.
|
||||
type Config struct {
|
||||
HTTPAddr string
|
||||
DatabaseURL string
|
||||
AppEnv string
|
||||
}
|
||||
|
||||
// Load reads configuration from the environment with safe defaults for local dev.
|
||||
func Load() Config {
|
||||
return Config{
|
||||
HTTPAddr: getenv("HTTP_ADDR", ":8080"),
|
||||
DatabaseURL: getenv("DATABASE_URL", "postgres://yuxingu:yuxingu@127.0.0.1:5432/yuxingu?sslmode=disable"),
|
||||
AppEnv: getenv("APP_ENV", "dev"),
|
||||
}
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
|
||||
)
|
||||
|
||||
// HealthHandler serves liveness probes.
|
||||
type HealthHandler struct{}
|
||||
|
||||
// NewHealthHandler constructs HealthHandler.
|
||||
func NewHealthHandler() *HealthHandler {
|
||||
return &HealthHandler{}
|
||||
}
|
||||
|
||||
// Register mounts health routes on the engine or router group.
|
||||
func (h *HealthHandler) Register(r gin.IRoutes) {
|
||||
r.GET("/healthz", h.Healthz)
|
||||
}
|
||||
|
||||
// Healthz returns a simple OK payload for load balancers.
|
||||
func (h *HealthHandler) Healthz(c *gin.Context) {
|
||||
response.OK(c, gin.H{"status": "ok"})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RequestID attaches X-Request-ID to every request/response for tracing.
|
||||
func RequestID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.GetHeader("X-Request-ID")
|
||||
if id == "" {
|
||||
id = newID()
|
||||
}
|
||||
c.Set("request_id", id)
|
||||
c.Header("X-Request-ID", id)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func newID() string {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return "req-unknown"
|
||||
}
|
||||
return hex.EncodeToString(b[:])
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Package response provides the unified API envelope {code,message,data}.
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Body is the standard JSON response shape for all HTTP APIs.
|
||||
type Body struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// OK writes a success response with optional data.
|
||||
func OK(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Body{Code: 0, Message: "success", Data: data})
|
||||
}
|
||||
|
||||
// Fail writes a business error with HTTP status and app code.
|
||||
func Fail(c *gin.Context, httpStatus int, code int, message string) {
|
||||
c.JSON(httpStatus, Body{Code: code, Message: message})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# 愈心谷产品与业务文档
|
||||
|
||||
工程规则以仓库根目录 [`.ai/`](../../.ai/) 为准;本目录放**产品 / 商业 / 竞品分析 / ADR(业务向)**。
|
||||
|
||||
`apps/docs/standards/*` 已废弃,请改读 `.ai/`。
|
||||
|
||||
---
|
||||
|
||||
## 阅读顺序(产品重设计)
|
||||
|
||||
1. [analysis/cece-teardown.md](analysis/cece-teardown.md) — 测测拆解 → 愈心谷映射
|
||||
2. [`.ai/product/cece-feature-map.md`](../../.ai/product/cece-feature-map.md) — 测测 Feature Map(IA × 能力域)
|
||||
3. [`.ai/product/feature-map.md`](../../.ai/product/feature-map.md) — 愈心谷 Feature Map + MVP
|
||||
4. [prd-mvp.md](prd-mvp.md) — MVP 目标、IA、用户故事、API 切片、验收
|
||||
5. [business-model.md](business-model.md) — 三层收入与飞轮
|
||||
6. [product-roadmap.md](product-roadmap.md) — Phase A→D 竖切计划
|
||||
|
||||
域词汇:[`.ai/domain.md`](../../.ai/domain.md) · 领域图:[`.ai/domain/domain-map.md`](../../.ai/domain/domain-map.md)
|
||||
|
||||
---
|
||||
|
||||
## 其他
|
||||
|
||||
| 文档 | 说明 |
|
||||
|---|---|
|
||||
| [adr/](adr/) | 架构/选型 ADR(与 `.ai/adr/` 分工:业务向可放此处) |
|
||||
| 根目录 `立项文档.md` | 早期立项材料;冲突时以本目录 + `.ai/` 为准 |
|
||||
@@ -0,0 +1,23 @@
|
||||
# ADR 0001: Monorepo + Go API + Vue H5
|
||||
|
||||
## 状态
|
||||
|
||||
已采纳(2026-08-02)
|
||||
|
||||
## 背景
|
||||
|
||||
愈心谷需从静态 HTML 原型演进为可多端扩展的产品,并引入 AI 辅助开发;需统一规范与清晰边界。
|
||||
|
||||
## 决策
|
||||
|
||||
- 采用 npm workspaces Monorepo(`apps/*` + `packages/*`)。
|
||||
- 后端唯一实现:Go(gin)于 `apps/api`。
|
||||
- 当前主客户端:Vue3 + TS H5(`apps/user-h5`);小程序仅脚手架。
|
||||
- 多端网络层:`packages/sdk` 适配器模式。
|
||||
- Git:Trunk-Based + Conventional Commits。
|
||||
|
||||
## 后果
|
||||
|
||||
- 新功能不得继续堆在根目录 legacy HTML。
|
||||
- 共享类型与 SDK 降低后续小程序迁移成本。
|
||||
- 需维护 workspace 与 Go module 两套依赖工具链。
|
||||
@@ -0,0 +1,7 @@
|
||||
# ADR location moved
|
||||
|
||||
Canonical Architecture Decision Records live in:
|
||||
|
||||
**[../../../.ai/adr/](../../../.ai/adr/)**
|
||||
|
||||
This folder keeps historical notes only if needed; do not add new ADRs here.
|
||||
@@ -0,0 +1,79 @@
|
||||
# 竞品拆解:测测 → 愈心谷映射
|
||||
|
||||
来源:[测测 App 深度体验报告(人人都是产品经理)](https://www.woshipm.com/evaluating/6391148.html)
|
||||
下游:[PRD MVP](../prd-mvp.md) · [商业模式](../business-model.md) · [产品路线图](../product-roadmap.md) · [文档索引](../README.md)
|
||||
|
||||
原则:学结构,不抄神秘学主叙事;愈心谷差异 = **数字性格 + 中医体质 + 关系档案 + 节气陪伴**(合规:无疗效、无吉凶)。
|
||||
|
||||
域词汇以 [`.ai/domain.md`](../../../.ai/domain.md) 为准。
|
||||
|
||||
---
|
||||
|
||||
## 1. 测测做对了什么(可迁移)
|
||||
|
||||
| 测测能力 | 本质 | 愈心谷对应 |
|
||||
|---|---|---|
|
||||
| 趣味测评拉新(MBTI 等) | 社交传播入口 | **Scale** + 分享卡片 |
|
||||
| 生日建档 /「了解 TA」 | 关系数据资产 | **Profile**(Self / Other) |
|
||||
| 五 Tab,中间「问」突出 | 战略入口可视化 | Home / Explore / **Ask** / Companion / Mine |
|
||||
| AI 挂档案,非通用闲聊 | 垂直差异化 | **Ask** 必须带 Profile 上下文 |
|
||||
| AI ‖ 真人 1v1 双轨 | 低摩擦升单 | Ask 内切换 **Consult**(后置) |
|
||||
| 会员订阅为主营收 | 可预测现金流 | **Subscription** → **Membership** / **VIP** |
|
||||
| 达人咨询高客单 | ARPU 拔高 | **Consult** |
|
||||
| 砍干扰广告 | 信任优先 | 不做干扰广告 |
|
||||
| 分享裂变 | 低 CAC | Decode / ScaleResult 分享 |
|
||||
|
||||
## 2. 测测的坑(愈心谷要避开)
|
||||
|
||||
| 风险 | 测测表现 | 愈心谷对策 |
|
||||
|---|---|---|
|
||||
| 定位拧巴 | 泛心理 vs 占星吸引力冲突 | 主叙事:文化测评 + 国标体质生活建议;重玄学模块降权 |
|
||||
| AI 像「算命」 | 引导问题偏运势 | Ask 按进入路径动态引导(性格/关系/养生) |
|
||||
| 视角切换难发现 | 「分析 TA」藏太深 | 首页解码后强引导建 Other Profile;Ask 顶部显式切换 Self/Other |
|
||||
| 跨会话记忆弱 | 电子闺蜜体验断裂 | MVP 先做 Profile 结构化记忆;长对话记忆 V1.2+ |
|
||||
| 多智能体认知负担 | 入口并列 | **只做一个 Ask**;不并行灵犀/小智式品牌矩阵 |
|
||||
|
||||
## 3. 信息架构对照
|
||||
|
||||
```
|
||||
测测: 首页 | 消息 | 问 | 在线 | 我的
|
||||
愈心谷: 首页 | 探索 | 问 | 陪伴 | 我的
|
||||
```
|
||||
|
||||
- **探索** ≈ 测测首页宫格 + AI 玩法广场的「内容供给」(MVP 不做 UGC 广场,只做官方 Scale / Decode 入口)
|
||||
- **陪伴** ≈ 每日心情 + 节气(测测心情打卡的留存位,换成 SolarTerm + Mood)
|
||||
- **消息 / 在线** 后置;Consult 先做预约表单,不做达人双边平台
|
||||
|
||||
## 4. 核心用户路径(对标测测三条场景)
|
||||
|
||||
### P1 自我探索(拉新)
|
||||
|
||||
Visitor → 首页 Decode 或 Scale → 简版结论 → 分享 → 注册为 User
|
||||
|
||||
### P2 关系决策(差异化)
|
||||
|
||||
User 建 Other Profile → Match 预览分数 → Unlock 解读 →(可选)Ask 切换 TA
|
||||
|
||||
### P3 情绪 / 养生留存(夜间场景)
|
||||
|
||||
Mood 打卡 / SolarTerm → Ask(档案上下文)→ 额度用尽 → Subscription
|
||||
|
||||
## 5. 能力分层(对标测测「情感陪伴层」)
|
||||
|
||||
```
|
||||
入口层(免费) Decode 简版 / Scale 部分 / Share
|
||||
留存层(VIP) Ask 额度 / SolarTerm 个性化 / Mood / 多 Profile
|
||||
转化层(高客单) Report Unlock / Match / Consult
|
||||
```
|
||||
|
||||
## 6. 不做清单(相对测测)
|
||||
|
||||
- 不自研大模型备案叙事(可接外部 LLM)
|
||||
- 不做硬件(巴布类)
|
||||
- 不做达人双边平台冷启动
|
||||
- 不做 AI 心情小镇 / 3D 沙盘(成本高,非差异化必需)
|
||||
- Legacy 根目录 HTML 不承接新需求(见 `.ai/architecture.md`)
|
||||
|
||||
## 7. 结论(一句话)
|
||||
|
||||
测测验证了「测评入口 → 档案化 AI → 订阅 + 咨询」飞轮;愈心谷用 **体质与节气** 替换「运势主叙事」,用 **家庭/关系 Profile** 做裂变,用 **合规生活建议** 守住审核与信任。
|
||||
@@ -0,0 +1,90 @@
|
||||
# 愈心谷商业模式
|
||||
|
||||
关联:[PRD MVP](prd-mvp.md) · [测测拆解](analysis/cece-teardown.md) · [产品路线图](product-roadmap.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话
|
||||
|
||||
用「数字性格 + 中医体质」做**可复算、可对照**的自我认知入口,以**订阅为主、报告/契合为辅、咨询为远期高客单**,承接测测式漏斗,但避开算命与医疗承诺。
|
||||
|
||||
---
|
||||
|
||||
## 2. 价值主张
|
||||
|
||||
| 对用户 | 对产品 |
|
||||
|---|---|
|
||||
| 知道「我是谁 / 我缺什么 / 怎么相处」 | 低摩擦获客(生日 / 量表) |
|
||||
| 比泛星座更「有结构」、比纯心理量表更「东方语境」 | 差异化:体质 × 性格 × 关系档案 |
|
||||
| 节气与日更内容形成回访理由 | 订阅续费钩子 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 收入三层(对齐测测,本地化)
|
||||
|
||||
```
|
||||
L1 免费获客 Decode 简版 / Scale / 分享卡
|
||||
↓
|
||||
L2 订阅留存 Membership:Ask 次数、完整报告、SolarTerm 深度、Mood 复盘
|
||||
↓
|
||||
L3 高 ARPU Unlock 单次报告/Match ·(远期)Consult
|
||||
```
|
||||
|
||||
| 层级 | 产品 | 定价方向(可调) | 备注 |
|
||||
|---|---|---|---|
|
||||
| L1 | 简版 Decode、热门 Scale | 0 | 结论可见 |
|
||||
| L2 | 月/季/年 VIP | 参考测测档位,偏下探试价 | 主收入 |
|
||||
| L2.5 | Unlock 完整报告 / Match 解读 | 单次低客单 | 不愿订也可转化 |
|
||||
| L3 | Consult | 远期 | 需履约与合规,非 MVP |
|
||||
|
||||
**不做:** 侵入式广告、诱导抽奖、伪医疗售卖。
|
||||
|
||||
---
|
||||
|
||||
## 4. 飞轮
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Share[分享卡/社交] --> Free[免费测评]
|
||||
Free --> Profile[档案沉淀]
|
||||
Profile --> Paywall[原因与方案付费墙]
|
||||
Paywall --> VIP[订阅]
|
||||
VIP --> Ask[问 / 陪伴回访]
|
||||
Ask --> Share
|
||||
Profile --> Match[关系档案]
|
||||
Match --> Unlock[单次解锁]
|
||||
Unlock --> VIP
|
||||
```
|
||||
|
||||
关键:Profile 越多 → Match/Ask 越有用 → 续费与解锁越高。
|
||||
|
||||
---
|
||||
|
||||
## 5. 单位经济(假设,待校验)
|
||||
|
||||
| 指标 | MVP 观测 |
|
||||
|---|---|
|
||||
| 免费完成率 | Decode 简版 / Scale 完成率 |
|
||||
| 付费转化 | 简版→Unlock 或 VIP |
|
||||
| ARPU | 订阅为主,Unlock 为辅 |
|
||||
| 留存 | D1/D7;节气打开率 |
|
||||
| 合规成本 | 客服投诉 / 违规文案拦截率 |
|
||||
|
||||
付费与权益**只信服务端**(见 `.ai/security.md`)。
|
||||
|
||||
---
|
||||
|
||||
## 6. B2B / 扩展(非 MVP)
|
||||
|
||||
企业 EAP、线下门店引流、内容授权——在 C 端漏斗跑通后再开。平台扩展路径:H5 → 小程序(同一 `packages/sdk`)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 与测测的差异化(商业)
|
||||
|
||||
| 测测 | 愈心谷 |
|
||||
|---|---|
|
||||
| 星盘+心理+社交玩法宽 | 数字性格+体质窄而深 |
|
||||
| 强娱乐/玄学氛围 | 可复算、生活建议、免责声明 |
|
||||
| 咨询达人网络 | 先模板 Ask,后 Consult |
|
||||
| UGC 广场 | 暂不做,避免内容治理成本 |
|
||||
@@ -0,0 +1,128 @@
|
||||
# 愈心谷 MVP PRD
|
||||
|
||||
关联:[测测拆解映射](analysis/cece-teardown.md) · [商业模式](business-model.md) · [产品路线图](product-roadmap.md)
|
||||
Feature Map:[`.ai/product/feature-map.md`](../../.ai/product/feature-map.md) · 竞品树:[`.ai/product/cece-feature-map.md`](../../.ai/product/cece-feature-map.md)
|
||||
域词汇:[`.ai/domain.md`](../../.ai/domain.md) · 领域图:[`.ai/domain/domain-map.md`](../../.ai/domain/domain-map.md)
|
||||
实现约束:[`.ai/architecture.md`](../../.ai/architecture.md) · playbooks: `add-api` / `new-page` / `payment`
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标 / 非目标
|
||||
|
||||
### 目标(MVP Done)
|
||||
|
||||
在 **user-h5 + api** 上跑通:
|
||||
|
||||
1. Visitor 输入生日 → 创建 **Self Profile** → 生成 **Decode** 简版(结论可见)
|
||||
2. 付费墙:Unlock 完整 Decode **或** 开通 **Subscription**(可模拟支付)
|
||||
3. 至少一个热门 **Scale**(如 MBTI)作答 → **ScaleResult** → 分享卡
|
||||
4. 可创建 **Other Profile**,生成 **Match** 预览(分数可见,解读锁定)
|
||||
5. 底栏 5 Tab 可用;Ask / Companion 允许规则模板占位,但入口与文案就位
|
||||
6. 服务端可复算报告;客户端不信任本地解锁
|
||||
|
||||
### 非目标
|
||||
|
||||
- Consult 履约 / 达人双边
|
||||
- mini-program 业务页
|
||||
- UGC「玩法广场」
|
||||
- 真实微信支付(用 `PAYMENT_MODE=mock`)
|
||||
- 长记忆多智能体 / 心情小镇级沉浸
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户与场景
|
||||
|
||||
| 画像 | 场景 | 成功指标 |
|
||||
|---|---|---|
|
||||
| P2 泛测评年轻女性 | 社交看到分享 → 测自己 | 完成 Decode 简版或 Scale |
|
||||
| P1 养生决策者 | 要「适合自己的调理参考」 | 查看完整 Decode 体质方案段 |
|
||||
| 关系困扰 | 帮 TA 测 / 看契合 | 创建 Other Profile + Match 预览 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 信息架构(5 Tab)
|
||||
|
||||
| Tab | 路由建议 | 内容 |
|
||||
|---|---|---|
|
||||
| 首页 | `/` | 品牌 + Decode 输入 + 热门 Feed + 宫格 |
|
||||
| 探索 | `/explore` | Scale 列表、Decode 入口;星镜/数字心理轻入口 |
|
||||
| 问 | `/ask` | Ask:基于当前 Profile;Tab 旁路「顾问预约」占位 |
|
||||
| 陪伴 | `/companion` | 今日 SolarTerm + Mood 打卡(可静态内容) |
|
||||
| 我的 | `/mine` | Profiles、Membership、Orders、设置 |
|
||||
|
||||
二级:`/decode` `/scales/:slug` `/match` `/report/:id`
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键用户故事
|
||||
|
||||
### US1 Decode
|
||||
|
||||
- 输入 year/month/day → API 校验 → 落 Profile + Report(Decode)
|
||||
- 简版:性格一句话 + 体质倾向一句话 + 1 条生活建议
|
||||
- 完整:原因拆解 + 方案列表;需 Unlock 或 VIP
|
||||
- 文案禁疗效 / 吉凶;页脚免责声明
|
||||
|
||||
### US2 Scale
|
||||
|
||||
- 列表来自 API;作答进度可本地缓存;提交服务端计分
|
||||
- 结果页:类型/分数 + 分享 + 引导 Decode/Ask
|
||||
|
||||
### US3 Other + Match
|
||||
|
||||
- 「帮 TA 测」创建 Other Profile
|
||||
- Match:展示契合分;解读段落锁定至 Unlock
|
||||
|
||||
### US4 Membership(mock)
|
||||
|
||||
- 套餐展示;下单 → mock 支付成功 → Membership active
|
||||
- Ask/完整报告次数按权益扣减(服务端)
|
||||
|
||||
---
|
||||
|
||||
## 5. 付费墙原则
|
||||
|
||||
**免费给结论,付费给原因 + 方案。**
|
||||
VIP 与单次 Unlock 二选一可达完整内容。
|
||||
|
||||
---
|
||||
|
||||
## 6. API 切片(MVP)
|
||||
|
||||
统一信封见 `.ai/api.md`。最小集合:
|
||||
|
||||
| Method | Path | 说明 |
|
||||
|---|---|---|
|
||||
| POST | `/api/v1/profiles` | 创建 Self/Other Profile |
|
||||
| GET | `/api/v1/profiles` | 列表 |
|
||||
| POST | `/api/v1/reports/decode` | 生成 Decode(简/全由权益决定) |
|
||||
| GET | `/api/v1/reports/:id` | 报告详情 |
|
||||
| GET | `/api/v1/scales` | 量表列表 |
|
||||
| GET | `/api/v1/scales/:slug` | 题目 |
|
||||
| POST | `/api/v1/scales/:slug/result` | 提交作答 |
|
||||
| POST | `/api/v1/match` | 契合度预览/完整 |
|
||||
| POST | `/api/v1/orders` | 创建订单 |
|
||||
| POST | `/api/v1/orders/:id/pay-mock` | 模拟支付 |
|
||||
| GET | `/api/v1/membership/me` | 当前权益 |
|
||||
| GET | `/api/v1/solar-terms/today` | 今日节气 |
|
||||
| POST | `/api/v1/moods` | 心情打卡(可后置) |
|
||||
|
||||
Auth:Visitor 可用 device token;注册后升级 User(见 ADR-0004)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收(QA)
|
||||
|
||||
- [ ] 未付费看不到完整 Decode 原因段(抓包改本地也不生效)
|
||||
- [ ] 模拟支付后同一 Report 可完整查看
|
||||
- [ ] Other Profile + Match 预览分数、解读锁定
|
||||
- [ ] Scale 提交返回稳定计分
|
||||
- [ ] 文案无疗效/吉凶;有免责声明
|
||||
- [ ] `go test ./...` 与 `npm run build:h5` 通过
|
||||
- [ ] `/api/v1/healthz` OK
|
||||
|
||||
---
|
||||
|
||||
## 8. 实现顺序(竖切)
|
||||
|
||||
按 [product-roadmap.md](product-roadmap.md) Phase A→B→C;每刀走 `.ai/playbooks/add-api.md` + `new-page.md`。
|
||||
@@ -0,0 +1,78 @@
|
||||
# 产品分期路线图
|
||||
|
||||
关联:[PRD MVP](prd-mvp.md) · [商业模式](business-model.md) · [`.ai/ROADMAP.md`](../../.ai/ROADMAP.md)(工程体系分期)
|
||||
|
||||
实现一律竖切:API → types → H5;playbook:`add-api` / `new-page` / `payment`。
|
||||
|
||||
---
|
||||
|
||||
## Phase A — 解码闭环(当前下一刀)
|
||||
|
||||
**目标:** Visitor 生日 → Profile → Decode 简/全权益 → mock Unlock/VIP
|
||||
|
||||
| 交付 | Playbook | Done |
|
||||
|---|---|---|
|
||||
| Profile CRUD API | add-api | |
|
||||
| Decode 生成 + Report 读取(服务端裁剪字段) | add-api | |
|
||||
| H5 首页表单接真 API + 报告页 | new-page | |
|
||||
| Order + pay-mock + Membership | payment | |
|
||||
| 免责声明与文案禁区 | review checklist | |
|
||||
|
||||
**退出标准:** PRD §7 中 Decode + Membership 相关项打勾。
|
||||
|
||||
---
|
||||
|
||||
## Phase B — 量表与关系
|
||||
|
||||
| 交付 | Playbook |
|
||||
|---|---|
|
||||
| Scales 列表/题目/计分 API + 1 个热门量表 | add-api + new-page |
|
||||
| Other Profile + Match 预览/锁定 | add-api + new-page |
|
||||
| 分享卡(静态图或 canvas) | new-page |
|
||||
| 探索 Tab 接真数据 | new-page |
|
||||
|
||||
**退出标准:** PRD US2/US3 验收通过。
|
||||
|
||||
---
|
||||
|
||||
## Phase C — 问与陪伴骨架
|
||||
|
||||
| 交付 | 说明 |
|
||||
|---|---|
|
||||
| Ask:规则模板 + 权益次数 | 非 LLM 亦可;接口形状稳定 |
|
||||
| SolarTerm 今日内容 API + 陪伴页 | 可先静态 CMS JSON |
|
||||
| Mood 打卡最小写 | 可选 |
|
||||
| 「顾问预约」占位页 | 不履约 |
|
||||
|
||||
**退出标准:** 5 Tab 均有真实或稳定占位数据;无死链。
|
||||
|
||||
---
|
||||
|
||||
## Phase D — 增长与平台(post-MVP)
|
||||
|
||||
- 真实支付通道(微信支付等)
|
||||
- Auth 升级、多端登录
|
||||
- mini-program 适配(`packages/sdk`)
|
||||
- Consult 试点(合规评审后)
|
||||
- 内容运营后台(admin-h5)最小可用
|
||||
|
||||
---
|
||||
|
||||
## 不做清单(直到明确开闸)
|
||||
|
||||
- UGC 玩法广场 / 达人市场
|
||||
- GraphQL / 微服务拆分
|
||||
- 长记忆多智能体陪伴
|
||||
- 根目录遗留 HTML 继续加功能(见 `LEGACY.md`)
|
||||
|
||||
---
|
||||
|
||||
## 与工程路线对齐
|
||||
|
||||
| 产品 Phase | 工程侧重 |
|
||||
|---|---|
|
||||
| A–B | `apps/api` + `apps/user-h5` + Postgres |
|
||||
| C | Redis 仍可延迟;Ask 限流按需 |
|
||||
| D | 支付真通道、小程序、admin |
|
||||
|
||||
工程体系 Phase 2/3(anti-patterns、metrics、mcp…)见 `.ai/ROADMAP.md`,不阻塞产品 A–C。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 01 Git 规范
|
||||
|
||||
## 策略
|
||||
|
||||
Trunk-Based Development:
|
||||
|
||||
- 默认分支:`main`(可发布)。
|
||||
- 功能分支:`feature/<short-name>`,生命周期尽量 < 2 天。
|
||||
- 修复:`fix/<short-name>`。
|
||||
- 不设长期 `develop`。
|
||||
|
||||
## Commit
|
||||
|
||||
Conventional Commits:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
[optional body]
|
||||
```
|
||||
|
||||
常用 type:`feat` `fix` `refactor` `perf` `style` `docs` `test` `chore`。
|
||||
|
||||
scope 示例:`auth` `report` `sdk` `user-h5` `api`。
|
||||
|
||||
subject 用英文或中文均可,但同一仓库保持一种;推荐英文短句,说明意图。
|
||||
|
||||
## Release
|
||||
|
||||
- 打 tag:`v0.1.0`(semver)。
|
||||
- Changelog 记录在 `apps/docs/CHANGELOG.md`(有正式发布时更新)。
|
||||
|
||||
## 禁止
|
||||
|
||||
- `git commit --no-verify`(除非负责人明确批准)。
|
||||
- 强制推送 `main`。
|
||||
- 提交密钥、`.env`、本地数据库文件。
|
||||
@@ -0,0 +1,66 @@
|
||||
# 02 Go 编码规范
|
||||
|
||||
参考:Effective Go、Uber Go Style Guide、Google Go Style Guide。
|
||||
|
||||
## 目录
|
||||
|
||||
```
|
||||
apps/api/
|
||||
cmd/server/
|
||||
internal/
|
||||
handler/
|
||||
service/<domain>/ # auth, profile, report, order…
|
||||
repository/
|
||||
model/
|
||||
middleware/
|
||||
config/
|
||||
pkg/ # 可复用小库,禁止膨胀
|
||||
migrations/
|
||||
```
|
||||
|
||||
## 包职责
|
||||
|
||||
- 按 **业务域** 分包,不要 `package utils`。
|
||||
- 好:`order` `payment` `auth` `profile`。
|
||||
- `pkg/` 仅放与业务无关的通用能力(如 `response` 封装)。
|
||||
|
||||
## Interface 放在调用方
|
||||
|
||||
在 `handler`(或真正的调用包)定义 interface,service 实现之。
|
||||
避免在 service 包里堆 `interface.go` 再反向依赖。
|
||||
|
||||
## Context
|
||||
|
||||
所有可能阻塞或下游调用的方法,首参必须是 `context.Context`:
|
||||
|
||||
```go
|
||||
func (s *Service) Login(ctx context.Context, req LoginRequest) (*User, error)
|
||||
```
|
||||
|
||||
## 错误
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
return fmt.Errorf("create user: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
禁止裸 `return err` 且丢失上下文;禁止 `_ = err` 吞掉关键错误。
|
||||
|
||||
## 日志
|
||||
|
||||
结构化日志(zap 或统一 slog 封装)。禁止业务路径 `fmt.Println`。
|
||||
|
||||
```go
|
||||
logger.Info("user login", zap.Int64("uid", uid), zap.String("ip", ip))
|
||||
```
|
||||
|
||||
## 命名
|
||||
|
||||
- 文件:`snake` 不强制,Go 惯用短名;导出标识符 PascalCase。
|
||||
- 避免 `Manager` `Helper` `Util` 空泛命名。
|
||||
|
||||
## 测试
|
||||
|
||||
- `*_test.go` 与实现同包或 `package foo_test`。
|
||||
- 表驱动测试优先;外部依赖用 interface mock。
|
||||
@@ -0,0 +1,60 @@
|
||||
# 03 REST API 规范
|
||||
|
||||
## URL
|
||||
|
||||
- 前缀:`/api/v1`。
|
||||
- 资源名词复数、层级清晰:
|
||||
|
||||
```
|
||||
GET /api/v1/users
|
||||
GET /api/v1/users/:id
|
||||
POST /api/v1/users
|
||||
PUT /api/v1/users/:id
|
||||
DELETE /api/v1/users/:id
|
||||
```
|
||||
|
||||
动作型可用子路径:`POST /api/v1/reports/decode`。
|
||||
|
||||
## 统一响应
|
||||
|
||||
成功:
|
||||
|
||||
```json
|
||||
{ "code": 0, "message": "success", "data": {} }
|
||||
```
|
||||
|
||||
失败(HTTP 可用 4xx/5xx,body 仍带业务码):
|
||||
|
||||
```json
|
||||
{ "code": 10001, "message": "user not found" }
|
||||
```
|
||||
|
||||
## 错误码段(约定)
|
||||
|
||||
| 段 | 含义 |
|
||||
|---|---|
|
||||
| 0 | 成功 |
|
||||
| 1xxxx | 通用 / 参数 / 认证 |
|
||||
| 2xxxx | 用户与档案 |
|
||||
| 3xxxx | 报告与量表 |
|
||||
| 4xxxx | 订单与会员 |
|
||||
| 5xxxx | AI / 陪伴 |
|
||||
|
||||
具体码表维护在 `apps/docs/api-errors.md`(随功能追加)。
|
||||
|
||||
## 分页
|
||||
|
||||
```
|
||||
GET /api/v1/scales?page=1&page_size=20
|
||||
```
|
||||
|
||||
`data` 建议:
|
||||
|
||||
```json
|
||||
{ "list": [], "total": 0, "page": 1, "page_size": 20 }
|
||||
```
|
||||
|
||||
## 版本与文档
|
||||
|
||||
- 破坏性变更升 `/api/v2` 或并行兼容期。
|
||||
- 同步更新 `proto/openapi.yaml`。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 04 数据库规范
|
||||
|
||||
## 引擎
|
||||
|
||||
PostgreSQL(主库)。本地可用 Docker Compose。
|
||||
|
||||
## 命名
|
||||
|
||||
- 表:`snake_case` 单数或清晰业务名,如 `user_profile`、`payment_order`、`user_session`。
|
||||
- **禁止** `tbl_` / `t_` 前缀。
|
||||
- 字段:`snake_case`。
|
||||
|
||||
## 公共字段
|
||||
|
||||
每张业务表默认包含:
|
||||
|
||||
```
|
||||
id BIGSERIAL PRIMARY KEY -- 或 UUID,项目内统一一种
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
deleted_at TIMESTAMPTZ NULL -- 软删除
|
||||
```
|
||||
|
||||
## 迁移
|
||||
|
||||
- 使用 goose(或 golang-migrate),文件放 `apps/api/migrations/`。
|
||||
- 迁移必须可回滚(提供 `Down`)。
|
||||
- 禁止手改生产库结构而不留迁移文件。
|
||||
|
||||
## 索引
|
||||
|
||||
- 高频查询字段建索引;唯一业务键用 UNIQUE。
|
||||
- 写迁移时注明索引意图。
|
||||
|
||||
## 敏感数据
|
||||
|
||||
- 生日等:最小化存储;访问打审计日志(后置也可先文档约定)。
|
||||
- 密钥、支付信息不得明文进业务表。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 05 H5 / Vue 规范
|
||||
|
||||
适用:`apps/user-h5`、`apps/admin-h5`。
|
||||
|
||||
## 栈
|
||||
|
||||
Vue 3 + TypeScript + Vite + Vue Router + Pinia。
|
||||
|
||||
## 目录
|
||||
|
||||
```
|
||||
src/
|
||||
components/
|
||||
pages/
|
||||
layouts/
|
||||
hooks/
|
||||
api/ # 仅薄封装 → @yuxingu/sdk
|
||||
stores/
|
||||
utils/
|
||||
router/
|
||||
assets/
|
||||
```
|
||||
|
||||
## 组件命名
|
||||
|
||||
- 文件与组件名:`PascalCase`,如 `UserCard.vue`、`OrderTable.vue`。
|
||||
- 禁止:`usercard.vue`、`aaa.vue`。
|
||||
|
||||
## 数据请求
|
||||
|
||||
- 页面 / 组件内禁止直接 `fetch`/`axios` 拼完整 URL。
|
||||
- 经 `src/api/*` → `@yuxingu/sdk`。
|
||||
|
||||
## 风格
|
||||
|
||||
- 移动优先;品牌色见 `packages/ui` token。
|
||||
- 单文件建议 ≤ 300 行,复杂页拆子组件与 composable(`hooks/`)。
|
||||
|
||||
## 路由
|
||||
|
||||
- 与产品 5 Tab 对齐:`/` `explore` `ask` `companion` `mine`;二级页如 `/decode`、`/scales/:slug`。
|
||||
@@ -0,0 +1,33 @@
|
||||
# 06 微信小程序规范
|
||||
|
||||
适用:`apps/mini-program`(业务后置,先遵守结构)。
|
||||
|
||||
## 目录建议
|
||||
|
||||
```
|
||||
pages/
|
||||
home/
|
||||
profile/
|
||||
login/
|
||||
setting/
|
||||
components/
|
||||
Avatar/
|
||||
Button/
|
||||
services/ # 业务请求,内部用 sdk 适配器
|
||||
user.ts
|
||||
order.ts
|
||||
utils/
|
||||
app.js / app.json / app.wxss
|
||||
```
|
||||
|
||||
## 规则
|
||||
|
||||
- **不要在页面里直接写请求**;统一 `services/*`。
|
||||
- 登录、存储通过适配器注入 `packages/sdk`(`wx.request` / `wx.setStorage`)。
|
||||
- 组件目录 PascalCase 或微信惯用名,项目内统一一种。
|
||||
- 与 H5 共享类型:`@yuxingu/types`;不共享 Vue SFC。
|
||||
|
||||
## 发布
|
||||
|
||||
- 使用独立 appid 配置,密钥不进库。
|
||||
- 体验版 / 正式版环境变量与 API baseURL 分离。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 07 TypeScript 规范
|
||||
|
||||
## 编译选项
|
||||
|
||||
`strict: true`(含 `noImplicitAny`、`strictNullChecks` 等)。
|
||||
|
||||
## 类型
|
||||
|
||||
- 优先 `interface` / `type` 描述领域对象;放 `packages/types` 供多端复用。
|
||||
- 禁止滥用 `any`;临时逃逸用 `unknown` + 收窄。
|
||||
- 避免 `as any`;确需断言写清原因注释。
|
||||
|
||||
## 示例
|
||||
|
||||
```ts
|
||||
export interface User {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
```
|
||||
|
||||
## 与 API
|
||||
|
||||
- 响应类型与 `code/message/data` 包装对齐 SDK 泛型。
|
||||
- 字段名与 JSON(snake_case)映射策略在 SDK 层统一(保持后端 snake,或统一 camel 转换,项目内二选一;**选定:前端 TS 用 camelCase,SDK 做转换**)。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 08 测试规范
|
||||
|
||||
## Go(`apps/api`)
|
||||
|
||||
- 领域纯函数(计分、解码规则)必须有单元测试。
|
||||
- Handler 可用 httptest 做集成测试;DB 用测试库或 docker。
|
||||
- CI 最少跑:`go test ./...`
|
||||
|
||||
## H5
|
||||
|
||||
- 关键工具函数单测(Vitest,后置可加)。
|
||||
- MVP 阶段:手动烟测清单写在 PR 描述(解码、量表、登录态)。
|
||||
|
||||
## E2E
|
||||
|
||||
- 正式流量前再引入 Playwright;不阻塞脚手架。
|
||||
|
||||
## 原则
|
||||
|
||||
- 测试与实现同 PR。
|
||||
- 不追求虚假覆盖率;优先测钱、测档案、测计分。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 09 Docker / CI/CD 规范
|
||||
|
||||
## 本地
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
服务:`postgres`(必备)、可选 `api` 镜像。
|
||||
|
||||
## 环境变量
|
||||
|
||||
- `APP_ENV`:`dev` | `staging` | `prod`
|
||||
- `DATABASE_URL`
|
||||
- `HTTP_ADDR`(默认 `:8080`)
|
||||
- `JWT_SECRET` / 支付密钥等仅环境注入
|
||||
|
||||
禁止把 `.env` 提交进 git(示例用 `.env.example`)。
|
||||
|
||||
## CI 门禁(目标)
|
||||
|
||||
1. `go test ./...`(api)
|
||||
2. `npm run build`(user-h5)
|
||||
3. lint(后置可加 golangci-lint / eslint)
|
||||
|
||||
## 镜像
|
||||
|
||||
- API 多阶段构建,最终镜像仅含二进制。
|
||||
- 标签:`yuxingu-api:<git-sha>`。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 10 AI 协作规范
|
||||
|
||||
配合根目录 [AGENTS.md](../../../AGENTS.md)。
|
||||
|
||||
## Prompt 约束(建议粘贴)
|
||||
|
||||
- 先读 AGENTS.md 与 ARCHITECTURE.md。
|
||||
- 只改任务相关文件;单 PR 单功能。
|
||||
- 单文件 ≤300 行,函数 ≤50 行。
|
||||
- Go interface 放调用方;H5 请求走 SDK。
|
||||
- 不要改 legacy 根目录 HTML,除非迁移任务。
|
||||
|
||||
## 评审清单
|
||||
|
||||
- [ ] 是否破坏分层?
|
||||
- [ ] 是否出现巨型 utils?
|
||||
- [ ] 错误是否包装 `%w`?
|
||||
- [ ] 是否泄露密钥或不合规文案?
|
||||
- [ ] 类型是否进了 `packages/types`?
|
||||
|
||||
## 禁止
|
||||
|
||||
- 让 AI「顺便」重构无关模块。
|
||||
- 无人工阅读直接合并大段生成代码。
|
||||
- 用 AI 生成后删除所有注释与测试。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 11 安全规范
|
||||
|
||||
## 认证与授权
|
||||
|
||||
- API 使用 Bearer Token(JWT 或 session);中间件校验。
|
||||
- 资源级校验:用户只能访问自己的档案/订单。
|
||||
|
||||
## 密钥
|
||||
|
||||
- 仅环境变量 / 密钥托管;禁止写进前端与仓库。
|
||||
- 轮换流程写入运维笔记(后置)。
|
||||
|
||||
## 输入校验
|
||||
|
||||
- 所有外部输入在 handler 层校验(生日范围、分页上限、字符串长度)。
|
||||
- SQL 一律参数化;禁止拼接。
|
||||
|
||||
## 隐私与合规
|
||||
|
||||
- 生日、测评结果属敏感个人信息:明示用途、支持删除(软删 + 注销流程后置)。
|
||||
- 对外文案:无医疗疗效、无吉凶祸福;报告页固定免责声明组件。
|
||||
|
||||
## 支付
|
||||
|
||||
- 验签、幂等、金额以服务端订单为准。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 12 文档规范
|
||||
|
||||
> **工程协作规则已迁至** [`.ai/`](../../../.ai/)。本文件仅作产品文档索引补充;标准正文勿再扩写于此。
|
||||
|
||||
## 类型
|
||||
|
||||
| 类型 | 位置 |
|
||||
|---|---|
|
||||
| 索引 | `apps/docs/README.md` |
|
||||
| 竞品分析 | `apps/docs/analysis/*` |
|
||||
| PRD | `apps/docs/prd-mvp.md` |
|
||||
| 商业模式 | `apps/docs/business-model.md` |
|
||||
| 产品路线图 | `apps/docs/product-roadmap.md` |
|
||||
| ADR(架构决策) | `apps/docs/adr/` 与 `.ai/adr/` |
|
||||
| API | `proto/openapi.yaml` |
|
||||
| 工程标准 | `.ai/*`(勿继续写 `apps/docs/standards/*`) |
|
||||
| 变更 | `apps/docs/CHANGELOG.md`(可选) |
|
||||
|
||||
## ADR
|
||||
|
||||
重大选型(库、支付渠道、LLM 供应商)必须写 ADR:背景、决策、后果。
|
||||
|
||||
## 文风
|
||||
|
||||
- 中文产品文档;英文可出现在代码标识符与 Commit。
|
||||
- 避免过时截图;以路径与接口为准。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Deprecated as source of truth
|
||||
|
||||
Engineering rules moved to the **AI Engineering System**:
|
||||
|
||||
→ **[../../../.ai/](../../../.ai/)**
|
||||
|
||||
| Old file | New location |
|
||||
|---|---|
|
||||
| 01-git | CONTRIBUTING.md + Trunk-Based note |
|
||||
| 02-go | `.ai/coding.md` + `.ai/tech-stack.md` |
|
||||
| 03-api | `.ai/api.md` |
|
||||
| 04-database | `.ai/database.md` |
|
||||
| 05-h5-vue | `.ai/ui.md` + `.ai/coding.md` |
|
||||
| 06-mini-program | `.ai/ui.md` + `.ai/architecture.md` |
|
||||
| 07-typescript | `.ai/coding.md` + `.ai/tech-stack.md` |
|
||||
| 08-testing | `.ai/testing.md` |
|
||||
| 09-cicd | `.ai/deployment.md` |
|
||||
| 10-ai-collab | `AGENTS.md` + `.ai/prompts/*` |
|
||||
| 11-security | `.ai/security.md` |
|
||||
| 12-docs | `.ai/definition-of-done.md` + `apps/docs` for PRD |
|
||||
|
||||
Keep PRD / business docs: `apps/docs/prd-mvp.md`, `apps/docs/business-model.md`.
|
||||
@@ -0,0 +1,23 @@
|
||||
# mini-program
|
||||
|
||||
微信小程序客户端(业务后置)。
|
||||
|
||||
## 接入约定
|
||||
|
||||
1. 使用 `packages/sdk` 的 `createClient`,注入微信适配器:
|
||||
|
||||
```ts
|
||||
// 伪代码
|
||||
createClient({
|
||||
baseURL: 'https://api.example.com',
|
||||
adapters: {
|
||||
request: (opts) => wxRequestToApiResponse(opts),
|
||||
getToken: () => wx.getStorageSync('yxg_token') || null,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
2. 页面禁止直接 `wx.request`;统一 `services/*`。
|
||||
3. 类型复用 `@yuxingu/types`;UI 组件不与 Vue H5 共享 SFC,只共享 `packages/ui` token。
|
||||
|
||||
详见 `apps/docs/standards/06-mini-program.md`。
|
||||
@@ -0,0 +1,5 @@
|
||||
App({
|
||||
onLaunch() {
|
||||
// scaffold only
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"pages": ["pages/home/index"],
|
||||
"window": {
|
||||
"navigationBarTitleText": "愈心谷",
|
||||
"navigationBarBackgroundColor": "#FFD1C7",
|
||||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Page({})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<view class="wrap">愈心谷小程序脚手架 — 业务后置</view>
|
||||
@@ -0,0 +1 @@
|
||||
.wrap{padding:24px;color:#666;font-size:14px}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no,viewport-fit=cover" />
|
||||
<title>愈心谷</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@yuxingu/user-h5",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yuxingu/sdk": "0.1.0",
|
||||
"@yuxingu/types": "0.1.0",
|
||||
"@yuxingu/ui": "0.1.0",
|
||||
"@yuxingu/utils": "0.1.0",
|
||||
"pinia": "^2.3.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.7",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<router-view />
|
||||
<TabBar v-if="showTab" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import TabBar from './components/TabBar.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const showTab = computed(() => route.meta.tab !== false)
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createClient, createBrowserAdapters } from '@yuxingu/sdk'
|
||||
|
||||
/** Shared API client for user-h5 (proxied to Go in dev). */
|
||||
export const api = createClient({
|
||||
baseURL: '',
|
||||
adapters: createBrowserAdapters(),
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<nav class="tabbar" aria-label="主导航">
|
||||
<router-link
|
||||
v-for="item in items"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
class="item"
|
||||
:class="{ ask: item.ask, on: isActive(item.to) }"
|
||||
>
|
||||
<span class="ni">{{ item.icon }}</span>
|
||||
{{ item.label }}
|
||||
</router-link>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const items = [
|
||||
{ to: '/', icon: '⌂', label: '首页' },
|
||||
{ to: '/explore', icon: '◎', label: '探索' },
|
||||
{ to: '/ask', icon: '问', label: '问', ask: true },
|
||||
{ to: '/companion', icon: '♡', label: '陪伴' },
|
||||
{ to: '/mine', icon: '◍', label: '我的' },
|
||||
]
|
||||
|
||||
function isActive(to: string): boolean {
|
||||
if (to === '/') return route.path === '/'
|
||||
return route.path.startsWith(to)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tabbar{
|
||||
position:fixed;bottom:0;left:50%;transform:translateX(-50%);
|
||||
width:100%;max-width:var(--yxg-max-w);background:#fff;
|
||||
display:flex;justify-content:space-around;
|
||||
padding:6px 0 calc(10px + env(safe-area-inset-bottom,0));
|
||||
border-top:1px solid #f0f0f0;z-index:100;
|
||||
}
|
||||
.item{
|
||||
flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;
|
||||
font-size:10px;color:var(--yxg-sub);padding:2px 0;
|
||||
}
|
||||
.item.on{color:var(--yxg-pri);font-weight:600}
|
||||
.ni{font-size:16px;line-height:1.2}
|
||||
.item.ask .ni{
|
||||
width:36px;height:36px;border-radius:50%;background:var(--yxg-pri);color:#fff;
|
||||
display:flex;align-items:center;justify-content:center;font-size:14px;margin-top:-10px;
|
||||
}
|
||||
.item.ask.on{color:var(--yxg-pri)}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import '@yuxingu/ui/tokens.css'
|
||||
import './styles/app.css'
|
||||
|
||||
createApp(App).use(createPinia()).use(router).mount('#app')
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<main class="page">
|
||||
<h1>问</h1>
|
||||
<p class="sub">愈心 AI · 基于档案的解读(占位)</p>
|
||||
<div class="card">后续接入档案上下文问答;与「真人顾问」双轨同屏。</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:24px 16px}
|
||||
h1{font-size:22px}
|
||||
.sub{color:var(--yxg-sub);font-size:13px;margin:6px 0 16px}
|
||||
.card{background:#fff;border-radius:16px;padding:18px;font-size:14px;color:#666;line-height:1.6}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<main class="page">
|
||||
<h1>陪伴</h1>
|
||||
<p class="sub">节气 · 每日心情(占位)</p>
|
||||
<div class="card">今日节气内容将由 API `/api/v1/solar-terms/today` 下发。</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:24px 16px}
|
||||
h1{font-size:22px}
|
||||
.sub{color:var(--yxg-sub);font-size:13px;margin:6px 0 16px}
|
||||
.card{background:#fff;border-radius:16px;padding:18px;font-size:14px;color:#666;line-height:1.6}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<main class="page">
|
||||
<button class="back" type="button" @click="$router.back()">← 返回</button>
|
||||
<h1>愈心解码</h1>
|
||||
<p class="sub" v-if="y">生日:{{ y }}-{{ m }}-{{ d }}</p>
|
||||
<div class="card">
|
||||
简版报告将由 Go API 生成。当前为页面骨架;完整「结论免费 / 原因付费」逻辑见 PRD。
|
||||
</div>
|
||||
<p class="disc">
|
||||
本内容为文化测评与健康生活方式参考,不构成医疗建议。
|
||||
</p>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const y = computed(() => String(route.query.y || ''))
|
||||
const m = computed(() => String(route.query.m || ''))
|
||||
const d = computed(() => String(route.query.d || ''))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:16px}
|
||||
.back{border:none;background:#fff;border-radius:10px;padding:8px 12px;margin-bottom:12px}
|
||||
h1{font-size:22px}
|
||||
.sub{color:var(--yxg-sub);margin:8px 0 14px;font-size:13px}
|
||||
.card{background:#fff;border-radius:16px;padding:18px;font-size:14px;line-height:1.7;color:#555}
|
||||
.disc{font-size:11px;color:#bbb;margin-top:16px;line-height:1.5}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<main class="page">
|
||||
<h1>探索</h1>
|
||||
<p class="sub">量表 · 数字心理 · 星镜(逐步迁入)</p>
|
||||
<ul class="list">
|
||||
<li>性格测评库(对接 /api/v1/scales)</li>
|
||||
<li>数字心理学</li>
|
||||
<li>星镜</li>
|
||||
</ul>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:24px 16px}
|
||||
h1{font-size:22px}
|
||||
.sub{color:var(--yxg-sub);font-size:13px;margin:6px 0 16px}
|
||||
.list{background:#fff;border-radius:16px;padding:8px 16px;line-height:2.2;font-size:14px;color:#555}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<main class="home">
|
||||
<header class="brand">
|
||||
<div class="word">愈<span>心</span>谷</div>
|
||||
<p class="tag">愈见自己 · 遇见更好</p>
|
||||
</header>
|
||||
|
||||
<section class="decode-card">
|
||||
<h2>愈心解码</h2>
|
||||
<p class="sub">一个生日,读懂你的身与心</p>
|
||||
<div class="row">
|
||||
<input v-model="year" type="number" placeholder="1990" />
|
||||
<span>年</span>
|
||||
<input v-model="month" type="number" placeholder="1" />
|
||||
<span>月</span>
|
||||
<input v-model="day" type="number" placeholder="1" />
|
||||
<span>日</span>
|
||||
<button type="button" @click="goDecode">生成</button>
|
||||
</div>
|
||||
<p v-if="err" class="err">{{ err }}</p>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<div class="head"><h3>认识自己</h3><router-link to="/explore">全部</router-link></div>
|
||||
<div class="grid">
|
||||
<router-link v-for="t in tests" :key="t.to" :to="t.to" class="cell">
|
||||
<div class="icon" :style="{ background: t.bg, color: t.fg }">{{ t.icon }}</div>
|
||||
<span>{{ t.label }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<div class="head"><h3>服务状态</h3></div>
|
||||
<p class="api">API:{{ apiStatus }}</p>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { validateBirth } from '@yuxingu/utils'
|
||||
import { api } from '../api/client'
|
||||
|
||||
const router = useRouter()
|
||||
const year = ref('')
|
||||
const month = ref('')
|
||||
const day = ref('')
|
||||
const err = ref('')
|
||||
const apiStatus = ref('检测中…')
|
||||
|
||||
const tests = [
|
||||
{ to: '/decode', icon: '△', label: '愈心解码', bg: '#FFF3D6', fg: '#C8923A' },
|
||||
{ to: '/explore', icon: '◆', label: '性格测评', bg: '#E3EEFF', fg: '#4A90E2' },
|
||||
{ to: '/ask', icon: '问', label: '愈心问答', bg: '#FFE4E4', fg: '#E54D42' },
|
||||
{ to: '/companion', icon: '☯', label: '节气陪伴', bg: '#E0F6E4', fg: '#5CB85C' },
|
||||
]
|
||||
|
||||
function goDecode() {
|
||||
const y = Number(year.value)
|
||||
const m = Number(month.value)
|
||||
const d = Number(day.value)
|
||||
const msg = validateBirth(y, m, d)
|
||||
if (msg) {
|
||||
err.value = msg
|
||||
return
|
||||
}
|
||||
err.value = ''
|
||||
router.push({ path: '/decode', query: { y: String(y), m: String(m), d: String(d) } })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await api.healthz()
|
||||
apiStatus.value = data.status === 'ok' ? '已连接' : '异常'
|
||||
} catch {
|
||||
apiStatus.value = '未连接(请启动 apps/api)'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home{padding:18px 16px 8px}
|
||||
.brand{text-align:center;margin-bottom:14px}
|
||||
.word{font-size:28px;font-weight:700;letter-spacing:.18em}
|
||||
.word span{color:var(--yxg-pri)}
|
||||
.tag{font-size:12px;color:rgba(0,0,0,.38);margin-top:6px;letter-spacing:.12em}
|
||||
.decode-card{
|
||||
background:#fff;border-radius:20px;padding:18px 14px;text-align:center;
|
||||
box-shadow:0 8px 28px rgba(229,77,66,.12);
|
||||
}
|
||||
.decode-card h2{font-size:17px;color:var(--yxg-gold);letter-spacing:.1em}
|
||||
.sub{font-size:12px;color:#aaa;margin:4px 0 14px}
|
||||
.row{display:flex;gap:6px;align-items:center;justify-content:center;flex-wrap:wrap}
|
||||
.row input{
|
||||
width:56px;padding:10px 4px;border:1.5px solid #eee;border-radius:12px;
|
||||
text-align:center;font-size:15px;outline:none;
|
||||
}
|
||||
.row button{
|
||||
padding:10px 16px;border:none;border-radius:22px;color:#fff;font-weight:600;
|
||||
background:linear-gradient(135deg,#ff7a6e,var(--yxg-pri));
|
||||
}
|
||||
.err{color:var(--yxg-pri);font-size:12px;margin-top:8px}
|
||||
.block{margin-top:22px;background:#fff;border-radius:18px;padding:16px;box-shadow:0 2px 10px rgba(0,0,0,.04)}
|
||||
.head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
|
||||
.head h3{font-size:16px}
|
||||
.head a{font-size:12px;color:#bbb}
|
||||
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px 8px}
|
||||
.cell{display:flex;flex-direction:column;align-items:center;gap:8px;font-size:11px;color:#555}
|
||||
.icon{
|
||||
width:52px;height:52px;border-radius:18px;display:flex;align-items:center;justify-content:center;font-size:20px;
|
||||
}
|
||||
.api{font-size:13px;color:var(--yxg-sub)}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<main class="page">
|
||||
<h1>我的</h1>
|
||||
<p class="sub">档案 · 会员 · 订单</p>
|
||||
<div class="card">登录与会员状态将在商业化阶段接入。</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:24px 16px}
|
||||
h1{font-size:22px}
|
||||
.sub{color:var(--yxg-sub);font-size:13px;margin:6px 0 16px}
|
||||
.card{background:#fff;border-radius:16px;padding:18px;font-size:14px;color:#666;line-height:1.6}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: () => import('../pages/HomePage.vue'), meta: { tab: true } },
|
||||
{ path: '/explore', name: 'explore', component: () => import('../pages/ExplorePage.vue'), meta: { tab: true } },
|
||||
{ path: '/ask', name: 'ask', component: () => import('../pages/AskPage.vue'), meta: { tab: true } },
|
||||
{ path: '/companion', name: 'companion', component: () => import('../pages/CompanionPage.vue'), meta: { tab: true } },
|
||||
{ path: '/mine', name: 'mine', component: () => import('../pages/MinePage.vue'), meta: { tab: true } },
|
||||
{ path: '/decode', name: 'decode', component: () => import('../pages/DecodePage.vue'), meta: { tab: false } },
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,12 @@
|
||||
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
|
||||
html,body,#app{min-height:100%}
|
||||
body{
|
||||
font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
|
||||
background:linear-gradient(180deg,var(--yxg-bg-start) 0%,var(--yxg-bg-end) 28%,#fff9f7 100%);
|
||||
color:var(--yxg-text);
|
||||
}
|
||||
.app-shell{
|
||||
max-width:var(--yxg-max-w);margin:0 auto;min-height:100vh;
|
||||
padding-bottom:calc(var(--yxg-nav-h) + env(safe-area-inset-bottom,0px) + 12px);
|
||||
}
|
||||
a{color:inherit;text-decoration:none}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<object, object, unknown>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user