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:
jackyu66git
2026-08-02 16:00:44 +08:00
co-authored by Cursor
parent 2686866376
commit 2fb1dfee14
193 changed files with 8854 additions and 1851 deletions
+3
View File
@@ -0,0 +1,3 @@
# admin-h5
运营后台(内容库、订单、会员)。**业务后置**,当前仅占位。
+6
View File
@@ -0,0 +1,6 @@
{
"name": "@yuxingu/admin-h5",
"version": "0.1.0",
"private": true,
"description": "运营后台占位,业务后置"
}
+39
View File
@@ -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)
}
}
+34
View File
@@ -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
)
+89
View File
@@ -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=
+27
View File
@@ -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
}
+25
View File
@@ -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[:])
}
+25
View File
@@ -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})
}
+27
View File
@@ -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 MapIA × 能力域)
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/` 为准 |
+23
View File
@@ -0,0 +1,23 @@
# ADR 0001: Monorepo + Go API + Vue H5
## 状态
已采纳(2026-08-02
## 背景
愈心谷需从静态 HTML 原型演进为可多端扩展的产品,并引入 AI 辅助开发;需统一规范与清晰边界。
## 决策
- 采用 npm workspaces Monorepo`apps/*` + `packages/*`)。
- 后端唯一实现:Gogin)于 `apps/api`
- 当前主客户端:Vue3 + TS H5`apps/user-h5`);小程序仅脚手架。
- 多端网络层:`packages/sdk` 适配器模式。
- GitTrunk-Based + Conventional Commits。
## 后果
- 新功能不得继续堆在根目录 legacy HTML。
- 共享类型与 SDK 降低后续小程序迁移成本。
- 需维护 workspace 与 Go module 两套依赖工具链。
+7
View File
@@ -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.
+79
View File
@@ -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 ProfileAsk 顶部显式切换 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** 做裂变,用 **合规生活建议** 守住审核与信任。
+90
View File
@@ -0,0 +1,90 @@
# 愈心谷商业模式
关联:[PRD MVP](prd-mvp.md) · [测测拆解](analysis/cece-teardown.md) · [产品路线图](product-roadmap.md)
---
## 1. 一句话
用「数字性格 + 中医体质」做**可复算、可对照**的自我认知入口,以**订阅为主、报告/契合为辅、咨询为远期高客单**,承接测测式漏斗,但避开算命与医疗承诺。
---
## 2. 价值主张
| 对用户 | 对产品 |
|---|---|
| 知道「我是谁 / 我缺什么 / 怎么相处」 | 低摩擦获客(生日 / 量表) |
| 比泛星座更「有结构」、比纯心理量表更「东方语境」 | 差异化:体质 × 性格 × 关系档案 |
| 节气与日更内容形成回访理由 | 订阅续费钩子 |
---
## 3. 收入三层(对齐测测,本地化)
```
L1 免费获客 Decode 简版 / Scale / 分享卡
L2 订阅留存 MembershipAsk 次数、完整报告、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 广场 | 暂不做,避免内容治理成本 |
+128
View File
@@ -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 Membershipmock
- 套餐展示;下单 → 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` | 心情打卡(可后置) |
AuthVisitor 可用 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`
+78
View File
@@ -0,0 +1,78 @@
# 产品分期路线图
关联:[PRD MVP](prd-mvp.md) · [商业模式](business-model.md) · [`.ai/ROADMAP.md`](../../.ai/ROADMAP.md)(工程体系分期)
实现一律竖切:API → types → H5playbook`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 | 工程侧重 |
|---|---|
| AB | `apps/api` + `apps/user-h5` + Postgres |
| C | Redis 仍可延迟;Ask 限流按需 |
| D | 支付真通道、小程序、admin |
工程体系 Phase 2/3anti-patterns、metrics、mcp…)见 `.ai/ROADMAP.md`,不阻塞产品 AC。
+37
View File
@@ -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`、本地数据库文件。
+66
View File
@@ -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`(或真正的调用包)定义 interfaceservice 实现之。
避免在 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。
+60
View File
@@ -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/5xxbody 仍带业务码):
```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`
+38
View File
@@ -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。
- 写迁移时注明索引意图。
## 敏感数据
- 生日等:最小化存储;访问打审计日志(后置也可先文档约定)。
- 密钥、支付信息不得明文进业务表。
+41
View File
@@ -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`
+33
View File
@@ -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 分离。
+25
View File
@@ -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 泛型。
- 字段名与 JSONsnake_case)映射策略在 SDK 层统一(保持后端 snake,或统一 camel 转换,项目内二选一;**选定:前端 TS 用 camelCaseSDK 做转换**)。
+21
View File
@@ -0,0 +1,21 @@
# 08 测试规范
## Go`apps/api`
- 领域纯函数(计分、解码规则)必须有单元测试。
- Handler 可用 httptest 做集成测试;DB 用测试库或 docker。
- CI 最少跑:`go test ./...`
## H5
- 关键工具函数单测(Vitest,后置可加)。
- MVP 阶段:手动烟测清单写在 PR 描述(解码、量表、登录态)。
## E2E
- 正式流量前再引入 Playwright;不阻塞脚手架。
## 原则
- 测试与实现同 PR。
- 不追求虚假覆盖率;优先测钱、测档案、测计分。
+29
View File
@@ -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>`
+25
View File
@@ -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 生成后删除所有注释与测试。
+25
View File
@@ -0,0 +1,25 @@
# 11 安全规范
## 认证与授权
- API 使用 Bearer TokenJWT 或 session);中间件校验。
- 资源级校验:用户只能访问自己的档案/订单。
## 密钥
- 仅环境变量 / 密钥托管;禁止写进前端与仓库。
- 轮换流程写入运维笔记(后置)。
## 输入校验
- 所有外部输入在 handler 层校验(生日范围、分页上限、字符串长度)。
- SQL 一律参数化;禁止拼接。
## 隐私与合规
- 生日、测评结果属敏感个人信息:明示用途、支持删除(软删 + 注销流程后置)。
- 对外文案:无医疗疗效、无吉凶祸福;报告页固定免责声明组件。
## 支付
- 验签、幂等、金额以服务端订单为准。
+26
View File
@@ -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。
- 避免过时截图;以路径与接口为准。
+22
View File
@@ -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`.
+23
View File
@@ -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`
+5
View File
@@ -0,0 +1,5 @@
App({
onLaunch() {
// scaffold only
},
})
+8
View File
@@ -0,0 +1,8 @@
{
"pages": ["pages/home/index"],
"window": {
"navigationBarTitleText": "愈心谷",
"navigationBarBackgroundColor": "#FFD1C7",
"navigationBarTextStyle": "black"
}
}
+1
View File
@@ -0,0 +1 @@
Page({})
+3
View File
@@ -0,0 +1,3 @@
{
"usingComponents": {}
}
+1
View File
@@ -0,0 +1 @@
<view class="wrap">愈心谷小程序脚手架 — 业务后置</view>
+1
View File
@@ -0,0 +1 @@
.wrap{padding:24px;color:#666;font-size:14px}
+12
View File
@@ -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>
+27
View File
@@ -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"
}
}
+15
View File
@@ -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>
+7
View File
@@ -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(),
})
+53
View File
@@ -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>
+8
View File
@@ -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')
+14
View File
@@ -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>
+14
View File
@@ -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>
+32
View File
@@ -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>
+18
View File
@@ -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>
+115
View File
@@ -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>
+14
View File
@@ -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>
+15
View File
@@ -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
+12
View File
@@ -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}
+7
View File
@@ -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
}
+24
View File
@@ -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"]
}
+21
View File
@@ -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,
},
},
},
})