feat(ECR-012): 星座对齐收口,并 Closed ECR-007/008
对齐 outlook/分享 type=star/报告页运势面板与测试;流程上关闭 Ops-B/C 两张 ECR。真支付仍后置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -232,8 +232,8 @@ UI **不出现「塔罗」**。禁止神谕吉凶、恐吓话术。
|
||||
| 分期 | 内容 |
|
||||
|---|---|
|
||||
| Phase A `[Ops]` | 登录 · 用户/订单查询 · 会员授予 · 审计 · `apps/admin-h5`(ECR-006 Closed) |
|
||||
| Phase B `[Ops]` | 行为分析:自有埋点 + 管理端「数据」看板(**ECR-007 Implemented** · Spec `ops-analytics.md`) |
|
||||
| Phase C `[Ops]` | 内容:首页宫格 CRUD · 测评上下架(**ECR-008 Implemented** · Spec `ops-content.md`) |
|
||||
| Phase B `[Ops]` | 行为分析:自有埋点 + 管理端「数据」看板(**ECR-007 Closed** · Spec `ops-analytics.md`) |
|
||||
| Phase C `[Ops]` | 内容:首页宫格 CRUD · 测评上下架(**ECR-008 Closed** · Spec `ops-content.md`) |
|
||||
| Phase D+ | 商业加深 · RBAC · 封禁 · 推送(各开独立 ECR) |
|
||||
|
||||
不计入 P1 Complete;不进入五 Tab。
|
||||
@@ -256,11 +256,11 @@ UI **不出现「塔罗」**。禁止神谕吉凶、恐吓话术。
|
||||
| `/scales/:slug` | 探索测试作答 | |
|
||||
| `/reports` | 成长报告列表 | |
|
||||
| `/reports/:id` | 成长报告详情 | |
|
||||
| `/star` | 星座 | P2 |
|
||||
| `/synastry` | 合盘 | P2 |
|
||||
| `/synastry/invite/:token` | 合盘邀请 | P2 |
|
||||
| `/rhythm` | 身心节律 | P2 draft |
|
||||
| `/cards` | 意象卡片 | P2 draft |
|
||||
| `/star` | 星座 | P2 · ECR-012 对齐中 |
|
||||
| `/synastry` | 合盘 | P2 · ECR-012 对齐中 |
|
||||
| `/synastry/invite/:token` | 合盘邀请 | P2 · ECR-012 对齐中 |
|
||||
| `/rhythm` | 身心节律 | P2(主路径已落地;非本 ECR) |
|
||||
| `/cards` | 意象卡片 | P2(主路径已落地;非本 ECR) |
|
||||
|
||||
兼容重定向:`/decode` → `/portrait`(仅技术兼容,UI 不出现 decode 文案)。
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
| 顺序 | 模块 | Spec | 路由 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| 0 | 探索目录 | — | `/explore` · `/explore/:category` | Done |
|
||||
| 1 | 星座 | [star-profile.md](star-profile.md) | `/star` | Done(星盘/运势/合盘对齐中) |
|
||||
| 1 | 星座 | [star-profile.md](star-profile.md) | `/star` | 主路径 Done · **ECR-012 Implemented(待 Closed)** |
|
||||
| 2 | 身心节律 | [life-rhythm.md](life-rhythm.md) | `/rhythm` | Done(今日/本周) |
|
||||
| 3 | 意象卡片 | [image-card.md](image-card.md) | `/cards` | Done(≥78 牌 · ≥8 场景) |
|
||||
| 4 | 量表矩阵 | [explore-test.md](explore-test.md) | `/scales/*` | Done(≥8) |
|
||||
@@ -28,4 +28,5 @@
|
||||
|---|---|
|
||||
| 设计三 Spec | Done |
|
||||
| 探索 catalog API | Done |
|
||||
| 真支付 | 部署阶段 |
|
||||
| 真支付 | 部署阶段(排期最后;非 ECR-012) |
|
||||
| 星座对齐 | **ECR-012 Implemented**(待 Reviewer Closed) |
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
| [share.md](share.md) | 分享卡 | journey 双引擎 | `/share` | P1 |
|
||||
| [companion.md](companion.md) | 陪伴(节气壳) | §4 | `/companion` | P1 壳 / P2 |
|
||||
| [analytics.md](analytics.md) | 增长埋点最小集 | 横切 | H5 `track` | P1 |
|
||||
| [star-profile.md](star-profile.md) | 星象性格 | §2.6 | `/star` | P2 设计 |
|
||||
| [star-profile.md](star-profile.md) | 星座 / 合盘 | §2.6 | `/star` · `/synastry` | P2 · ECR-012 Approved |
|
||||
| [life-rhythm.md](life-rhythm.md) | 身心节律 | §2.7 | `/rhythm` | P2 设计 |
|
||||
| [image-card.md](image-card.md) | 意象卡片 | §2.8 | `/cards` | P2 设计 |
|
||||
| [ops-admin.md](ops-admin.md) | 运营后台 | §7 | `admin-h5` `/` `/users/:id` … | Ops-A |
|
||||
| [ops-analytics.md](ops-analytics.md) | 运营行为分析(埋点+数据看板) | §7 | `admin-h5` `/analytics` · H5 track | Ops-B · ECR-007 Implemented |
|
||||
| [ops-content.md](ops-content.md) | 运营内容(宫格+测评上下架) | §7 | `admin-h5` `/content` · `GET /home/tools` | Ops-C · ECR-008 Approved |
|
||||
| [ops-analytics.md](ops-analytics.md) | 运营行为分析(埋点+数据看板) | §7 | `admin-h5` `/analytics` · H5 track | Ops-B · ECR-007 Closed |
|
||||
| [ops-content.md](ops-content.md) | 运营内容(宫格+测评上下架) | §7 | `admin-h5` `/content` · `GET /home/tools` | Ops-C · ECR-008 Closed |
|
||||
|
||||
新功能:复制 `_TEMPLATE.md` → 填满 → 在本表登记 → 再编码。
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ Phase A 可先 `console`/本地;不挡验收。
|
||||
| Phase | 内容 |
|
||||
|---|---|
|
||||
| **A(本 ECR)** | 登录 · 用户 · 订单 · 授予会员 · 审计 · admin-h5 壳 |
|
||||
| **B(ECR-007)** | 自有埋点 · 管理端「数据」看板 — Spec `ops-analytics.md` |
|
||||
| **C(ECR-008)** | 首页宫格 CRUD · 测评上下架 — Spec `ops-content.md`(Approved) |
|
||||
| **B(ECR-007 Closed)** | 自有埋点 · 管理端「数据」看板 — Spec `ops-analytics.md` |
|
||||
| **C(ECR-008 Closed)** | 首页宫格 CRUD · 测评上下架 — Spec `ops-content.md` |
|
||||
| D | 订单筛选 · 展示价 · 退款只读(另开 ECR) |
|
||||
| E | RBAC · 封禁 · 推送占位(另开 ECR) |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Feature Spec: 运营行为分析(Ops-B)
|
||||
|
||||
> Status: `Active` · Map: `§7 运营后台` · Phase: `Ops-B`
|
||||
> ESS:`docs/ECR/ECR-007-ops-analytics.md`(Approved · L2)· BD-2026-007 Approved
|
||||
> ESS:`docs/ECR/ECR-007-ops-analytics.md`(**Closed** · L2)· BD-2026-007 Approved
|
||||
> 关联:[ops-admin.md](ops-admin.md) · [analytics.md](analytics.md) · [cece-frontend-re/complete-design/40-ops-commerce.md](cece-frontend-re/complete-design/40-ops-commerce.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Feature Spec: 运营内容配置(Ops-C)
|
||||
|
||||
> Status: `Active` · Map: `§7 运营后台` · Phase: `Ops-C`
|
||||
> ESS:`docs/ECR/ECR-008-ops-content.md`(Approved · L2)· BD-2026-008 Approved
|
||||
> ESS:`docs/ECR/ECR-008-ops-content.md`(**Closed** · L2)· BD-2026-008 Approved
|
||||
> 关联:[ops-admin.md](ops-admin.md) · [ops-analytics.md](ops-analytics.md) · [home.md](home.md) · [explore-test.md](explore-test.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
|
||||
**Given** Self 有生日
|
||||
**When** 生成星座报告
|
||||
**Then** 展示轮盘、太阳/月亮/上升、相位速览、fortune 日运与行运;无「占卜」「算命」恐吓
|
||||
**Then** 展示轮盘、太阳/月亮/上升、相位速览、outlook 日/周/月/年/一生与行运;无「占卜」「算命」恐吓
|
||||
|
||||
**Given** 两份档案
|
||||
**When** POST synastry
|
||||
@@ -202,3 +202,5 @@
|
||||
| Packages | `internal/star` · `ephemeris` · `natal` · `fortune` · `synastry` |
|
||||
| 星历 | Swiss Ephemeris(CGO,默认 Moshier);热带 + Whole Sign |
|
||||
| 实现 | `POST /reports/star` · `POST /reports/synastry` · nearby/invites · H5 `/star` · `/synastry` · invite |
|
||||
| Gaps | ECR-012 收口:报告页复用 StarFortunePanel;分享 type=star;fortuneBundle 仅读 outlook 且剥离 lucky;e2e mock 含 outlook |
|
||||
| ESS | `docs/ECR/ECR-012-star-alignment.md`(**Approved** · L2)· BD-2026-012 Approved |
|
||||
|
||||
@@ -88,6 +88,9 @@ func TestFlowStarDeepAccess(t *testing.T) {
|
||||
if sum["fortune"] != nil {
|
||||
t.Fatal("legacy fortune key must be removed (ECR-003)")
|
||||
}
|
||||
if raw, _ := json.Marshal(sum); strings.Contains(string(raw), `"lucky"`) {
|
||||
t.Fatal("legacy lucky field must not appear in star summary (ECR-012)")
|
||||
}
|
||||
reportID := rep["id"].(string)
|
||||
|
||||
env, key = doJSON(t, r, http.MethodPost, "/api/v1/orders", map[string]any{
|
||||
|
||||
@@ -30,7 +30,27 @@ test('star / rhythm / cards pages render with mocked API', async ({ page }) => {
|
||||
id: 'rep-star',
|
||||
type: 'star',
|
||||
has_deep_access: false,
|
||||
summary: { headline: 'E2E星象', one_liner: '星象摘要', keywords: ['灵活'] },
|
||||
summary: {
|
||||
headline: 'E2E星象',
|
||||
one_liner: '星象摘要',
|
||||
keywords: ['灵活'],
|
||||
planets: [
|
||||
{ key: 'sun', title: '太阳', sign: '双子', degree: '10°', house: 1, lon: 70 },
|
||||
],
|
||||
chart: { note: '热带黄道', asc_lon: 70, houses: [{ num: 1, sign: '双子' }] },
|
||||
aspects_preview: [],
|
||||
outlook: {
|
||||
daily: {
|
||||
title: '今日运势', label: '顺畅', score: 80, tip: 'e2e tip',
|
||||
focus: '行动', boost: '水', caution: '缓', dims: { love: 3, career: 4, money: 3, mood: 4 },
|
||||
},
|
||||
weekly: { title: '本周', label: '稳', score: 70, tip: 'w', focus: 'f', boost: 'b', caution: 'c' },
|
||||
monthly: { title: '本月', label: '稳', score: 70, tip: 'm', focus: 'f', boost: 'b', caution: 'c' },
|
||||
yearly: { title: '今年', label: '稳', score: 70, tip: 'y', focus: 'f', boost: 'b', caution: 'c' },
|
||||
lifetime: { title: '一生运势摘要', label: '稳', score: 70, tip: 'l', focus: 'f', boost: 'b', caution: 'c' },
|
||||
},
|
||||
transits: [{ key: 't1', title: '行运提示', aspect: '合相', tip: '推进' }],
|
||||
},
|
||||
detail: null,
|
||||
created_at: new Date().toISOString(),
|
||||
}))
|
||||
@@ -86,6 +106,9 @@ test('star / rhythm / cards pages render with mocked API', async ({ page }) => {
|
||||
await page.goto('/psy/star?y=1983&m=6&d=6')
|
||||
await expect(page.getByText('E2E星象')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /解锁完整分析/ })).toBeVisible()
|
||||
await page.getByRole('button', { name: '运势' }).click()
|
||||
await expect(page.getByText('今日运势')).toBeVisible()
|
||||
await expect(page.getByText('焦点:行动')).toBeVisible()
|
||||
|
||||
await page.goto('/psy/rhythm?y=1992&m=6&d=8')
|
||||
await expect(page.getByText('身心节律').first()).toBeVisible()
|
||||
|
||||
@@ -20,7 +20,7 @@ import BrandLogo from './BrandLogo.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
type?: 'portrait' | 'relation'
|
||||
type?: 'portrait' | 'star' | 'relation'
|
||||
title?: string
|
||||
line?: string
|
||||
rows?: string[]
|
||||
@@ -72,4 +72,13 @@ h2{font-size:17px;font-weight:600;margin:0 0 8px;line-height:1.4}
|
||||
.share-card[data-type="relation"] .cta-label{
|
||||
background:linear-gradient(135deg,#e0b35a,#c8923a);
|
||||
}
|
||||
.share-card[data-type="star"]{
|
||||
background:
|
||||
radial-gradient(120% 80% at 100% 0%, rgba(100,120,200,.18), transparent 55%),
|
||||
linear-gradient(165deg, #f6f8ff 0%, #e8ecff 48%, #fff 100%);
|
||||
}
|
||||
.share-card[data-type="star"] .tags span{background:rgba(100,120,200,.12);color:#4a5a9a}
|
||||
.share-card[data-type="star"] .cta-label{
|
||||
background:linear-gradient(135deg,#7a8ad4,#4a5a9a);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,13 +32,13 @@ const status = ref('')
|
||||
const card = computed(() => {
|
||||
const p = props.payload
|
||||
if (!p) return {}
|
||||
if (p.type === 'portrait') {
|
||||
if (p.type === 'portrait' || p.type === 'star') {
|
||||
return {
|
||||
type: 'portrait' as const,
|
||||
type: p.type,
|
||||
title: p.title,
|
||||
line: p.line,
|
||||
keywords: p.keywords,
|
||||
ctaLabel: '查看完整成长报告',
|
||||
ctaLabel: p.type === 'star' ? '查看完整星座报告' : '查看完整成长报告',
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -56,6 +56,12 @@ const card = computed(() => {
|
||||
|
||||
const canNative = computed(() => typeof navigator !== 'undefined' && typeof navigator.share === 'function')
|
||||
|
||||
function nativeShareText(type: SharePayload['type']): string {
|
||||
if (type === 'relation') return '看看我们的相处方式'
|
||||
if (type === 'star') return '看看我的星座探索'
|
||||
return '看看我的个人画像'
|
||||
}
|
||||
|
||||
function absoluteShareURL(): string {
|
||||
if (!props.payload) return location.origin
|
||||
return `${location.origin}${buildSharePath(props.payload)}`
|
||||
@@ -76,7 +82,7 @@ async function nativeShare() {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: '愈心谷',
|
||||
text: props.payload.type === 'relation' ? '看看我们的相处方式' : '看看我的个人画像',
|
||||
text: nativeShareText(props.payload.type),
|
||||
url: absoluteShareURL(),
|
||||
})
|
||||
status.value = '已唤起系统分享'
|
||||
|
||||
@@ -17,27 +17,13 @@
|
||||
<p>{{ activeCard.teaser }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="panelKey === 'fortune'">
|
||||
<div class="fortune-tabs">
|
||||
<button
|
||||
v-for="k in fortuneKeys"
|
||||
:key="k"
|
||||
type="button"
|
||||
class="ftab"
|
||||
:class="{ on: fortuneKey === k }"
|
||||
@click="$emit('update:fortuneKey', k)"
|
||||
>
|
||||
{{ fortuneLabel(k) }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="activeFortune" class="spotlight">
|
||||
<p class="ft-title">
|
||||
{{ activeFortune.title }} · {{ activeFortune.label }} · {{ activeFortune.score }}分
|
||||
</p>
|
||||
<p>{{ activeFortune.tip }}</p>
|
||||
</div>
|
||||
<p v-else class="yxg-hint">暂无运势数据</p>
|
||||
</template>
|
||||
<StarFortunePanel
|
||||
v-else-if="panelKey === 'fortune'"
|
||||
:fortune-key="fortuneKey"
|
||||
:fortune-keys="fortuneKeys"
|
||||
:active-fortune="activeFortune"
|
||||
@update:fortune-key="$emit('update:fortuneKey', $event)"
|
||||
/>
|
||||
<ul v-else class="planet-list">
|
||||
<li v-for="p in planets" :key="p.key" class="planet-row">
|
||||
<strong>{{ p.title }}</strong>
|
||||
@@ -50,10 +36,10 @@
|
||||
<script setup lang="ts">
|
||||
import NatalWheel, { type WheelAspect, type WheelPlanet } from '../NatalWheel.vue'
|
||||
import SignCards, { type SignCard } from '../SignCards.vue'
|
||||
import { fortuneLabel } from '../../composables/useStarProfilePage'
|
||||
import StarFortunePanel from '../star/StarFortunePanel.vue'
|
||||
import type { FortunePeriod } from '../../lib/reportSummary'
|
||||
import { reportFortuneKeys } from '../../composables/useReportPage'
|
||||
|
||||
type FortunePeriod = { title?: string; label?: string; score?: number; tip?: string }
|
||||
type PlanetRow = { key: string; title: string; sign: string; degree: string; house: number }
|
||||
|
||||
defineProps<{
|
||||
@@ -88,19 +74,6 @@ const fortuneKeys = reportFortuneKeys
|
||||
}
|
||||
.spotlight h3 { font-size: 15px; margin-bottom: 6px; color: #222; }
|
||||
.spotlight p { font-size: 13px; color: #555; line-height: 1.6; }
|
||||
.fortune-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.ftab {
|
||||
flex: 1;
|
||||
min-width: 52px;
|
||||
border: 1px solid #eee;
|
||||
background: #fafafa;
|
||||
border-radius: 12px;
|
||||
padding: 10px 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.ftab.on { border-color: var(--color-primary); background: #fff5f4; color: #222; font-weight: 600; }
|
||||
.ft-title { font-weight: 700; color: #222; margin-bottom: 6px; }
|
||||
.planet-list { list-style: none; padding: 0; margin: 0; }
|
||||
.planet-row {
|
||||
display: flex;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<p class="daily-title">
|
||||
{{ activeFortune.title }} · {{ activeFortune.label }} · {{ activeFortune.score }}分
|
||||
</p>
|
||||
<p class="focus">焦点:{{ activeFortune.focus }}</p>
|
||||
<p v-if="activeFortune.focus" class="focus">焦点:{{ activeFortune.focus }}</p>
|
||||
<p>{{ activeFortune.tip }}</p>
|
||||
<div v-if="activeFortune.dims" class="dims">
|
||||
<span>感情 {{ activeFortune.dims.love }}</span>
|
||||
@@ -24,26 +24,16 @@
|
||||
<span>财运 {{ activeFortune.dims.money }}</span>
|
||||
<span>心情 {{ activeFortune.dims.mood }}</span>
|
||||
</div>
|
||||
<p class="yxg-meta">今日提示:{{ activeFortune.boost }}</p>
|
||||
<p class="yxg-meta">注意:{{ activeFortune.caution }}</p>
|
||||
<p v-if="activeFortune.boost" class="yxg-meta">今日提示:{{ activeFortune.boost }}</p>
|
||||
<p v-if="activeFortune.caution" class="yxg-meta">注意:{{ activeFortune.caution }}</p>
|
||||
</div>
|
||||
<p v-else class="yxg-hint">暂无运势数据</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fortuneLabel } from '../../composables/useStarProfilePage'
|
||||
|
||||
type FortunePeriod = {
|
||||
title?: string
|
||||
label?: string
|
||||
score?: number
|
||||
tip?: string
|
||||
focus?: string
|
||||
boost?: string
|
||||
lucky?: string
|
||||
caution?: string
|
||||
dims?: { love?: number; career?: number; money?: number; mood?: number }
|
||||
}
|
||||
import type { FortunePeriod } from '../../lib/reportSummary'
|
||||
|
||||
defineProps<{
|
||||
fortuneKey: string
|
||||
@@ -78,4 +68,5 @@ defineEmits<{
|
||||
.daily-title { font-weight: 700; color: #222; margin-bottom: 6px; }
|
||||
.focus { margin: 0 0 8px; color: #666; }
|
||||
.dims { display: flex; flex-wrap: wrap; gap: 10px; margin: 10px 0; font-size: 12px; color: #555; }
|
||||
.yxg-hint { margin: 8px 0; font-size: 13px; color: #999; }
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
fortuneBundleFrom,
|
||||
signCardsFrom,
|
||||
} from '../lib/reportSummary'
|
||||
import type { PortraitSharePayload } from '../lib/shareLink'
|
||||
import type { StarSharePayload } from '../lib/shareLink'
|
||||
import {
|
||||
aspectLinesFrom,
|
||||
aspectsFrom,
|
||||
@@ -80,9 +80,9 @@ export function useStarProfilePage() {
|
||||
aspectLinesFrom(report.value?.has_deep_access, fullAspects.value, aspectPreview.value),
|
||||
)
|
||||
const gridCells = computed(() => buildGridCells(signCards.value, planets.value))
|
||||
const sharePayload = computed<PortraitSharePayload | null>(() => {
|
||||
const sharePayload = computed<StarSharePayload | null>(() => {
|
||||
if (!report.value) return null
|
||||
return { type: 'portrait', title: headline.value || '我的星座', line: oneLiner.value, keywords: keywords.value }
|
||||
return { type: 'star', title: headline.value || '我的星座', line: oneLiner.value, keywords: keywords.value }
|
||||
})
|
||||
|
||||
function onWheelSelect(key: string) {
|
||||
|
||||
@@ -135,7 +135,27 @@ export function signCardsFrom(summary: Record<string, unknown>): SignCard[] {
|
||||
|
||||
export function fortuneBundleFrom(summary: Record<string, unknown>): Record<string, FortunePeriod> {
|
||||
const o = summary.outlook
|
||||
return o && typeof o === 'object' ? (o as Record<string, FortunePeriod>) : {}
|
||||
if (!o || typeof o !== 'object') return {}
|
||||
const src = o as Record<string, unknown>
|
||||
const out: Record<string, FortunePeriod> = {}
|
||||
for (const k of reportFortuneKeys) {
|
||||
const raw = src[k]
|
||||
if (raw && typeof raw === 'object') out[k] = periodFromRaw(raw as Record<string, unknown>)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function periodFromRaw(p: Record<string, unknown>): FortunePeriod {
|
||||
return {
|
||||
title: typeof p.title === 'string' ? p.title : undefined,
|
||||
label: typeof p.label === 'string' ? p.label : undefined,
|
||||
score: typeof p.score === 'number' ? p.score : undefined,
|
||||
tip: typeof p.tip === 'string' ? p.tip : undefined,
|
||||
focus: typeof p.focus === 'string' ? p.focus : undefined,
|
||||
boost: typeof p.boost === 'string' ? p.boost : undefined,
|
||||
caution: typeof p.caution === 'string' ? p.caution : undefined,
|
||||
dims: p.dims && typeof p.dims === 'object' ? (p.dims as FortunePeriod['dims']) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function planetsFrom(summary: Record<string, unknown>): PlanetRow[] {
|
||||
@@ -172,6 +192,9 @@ export function buildReportSharePayload(
|
||||
keywords,
|
||||
} as RelationSharePayload
|
||||
}
|
||||
if (report.type === 'star') {
|
||||
return { type: 'star', title: headline, line: oneLiner, keywords }
|
||||
}
|
||||
return {
|
||||
type: 'portrait',
|
||||
title: headline,
|
||||
|
||||
@@ -39,8 +39,25 @@ describe('shareLink', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('round-trips star payload', () => {
|
||||
const path = buildSharePath({
|
||||
type: 'star',
|
||||
title: '稳健沉淀者',
|
||||
line: '星座一句',
|
||||
keywords: ['金牛'],
|
||||
})
|
||||
const q = Object.fromEntries(new URLSearchParams(path.slice(path.indexOf('?'))))
|
||||
expect(parseShareQuery(q)).toEqual({
|
||||
type: 'star',
|
||||
title: '稳健沉淀者',
|
||||
line: '星座一句',
|
||||
keywords: ['金牛'],
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null for empty/invalid', () => {
|
||||
expect(parseShareQuery({})).toBeNull()
|
||||
expect(parseShareQuery({ type: 'portrait' })).toBeNull()
|
||||
expect(parseShareQuery({ type: 'star' })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,14 @@ export type PortraitSharePayload = {
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
/** Same card shape as portrait; type discriminates 星座 landing copy. */
|
||||
export type StarSharePayload = {
|
||||
type: 'star'
|
||||
title: string
|
||||
line: string
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
export type RelationSharePayload = {
|
||||
type: 'relation'
|
||||
me: string
|
||||
@@ -15,15 +23,22 @@ export type RelationSharePayload = {
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
export type SharePayload = PortraitSharePayload | RelationSharePayload
|
||||
export type SharePayload = PortraitSharePayload | StarSharePayload | RelationSharePayload
|
||||
|
||||
function appendTitleLineKeywords(
|
||||
q: URLSearchParams,
|
||||
payload: PortraitSharePayload | StarSharePayload,
|
||||
) {
|
||||
if (payload.title) q.set('t', payload.title)
|
||||
if (payload.line) q.set('l', payload.line)
|
||||
if (payload.keywords.length) q.set('k', payload.keywords.slice(0, 6).join(','))
|
||||
}
|
||||
|
||||
export function buildSharePath(payload: SharePayload): string {
|
||||
const q = new URLSearchParams()
|
||||
q.set('type', payload.type)
|
||||
if (payload.type === 'portrait') {
|
||||
if (payload.title) q.set('t', payload.title)
|
||||
if (payload.line) q.set('l', payload.line)
|
||||
if (payload.keywords.length) q.set('k', payload.keywords.slice(0, 6).join(','))
|
||||
if (payload.type === 'portrait' || payload.type === 'star') {
|
||||
appendTitleLineKeywords(q, payload)
|
||||
} else {
|
||||
if (payload.me) q.set('me', payload.me)
|
||||
if (payload.other) q.set('ot', payload.other)
|
||||
@@ -39,11 +54,11 @@ export function parseShareQuery(query: Record<string, unknown>): SharePayload |
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
if (type === 'portrait') {
|
||||
if (type === 'portrait' || type === 'star') {
|
||||
const title = String(query.t || '')
|
||||
const line = String(query.l || '')
|
||||
if (!title && !line && !keywords.length) return null
|
||||
return { type: 'portrait', title, line, keywords }
|
||||
return { type: type as 'portrait' | 'star', title, line, keywords }
|
||||
}
|
||||
if (type === 'relation') {
|
||||
const me = String(query.me || '')
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<template v-else>
|
||||
<div class="hero-block">
|
||||
<HomeToolIcon :name="payload.type === 'relation' ? 'relation' : 'portrait'" :size="64" />
|
||||
<HomeToolIcon :name="heroIcon" :size="64" />
|
||||
<h1 class="hero-title">{{ shareTitle }}</h1>
|
||||
<p class="hero-sub">{{ shareSub }}</p>
|
||||
</div>
|
||||
@@ -52,24 +52,35 @@ const payload = computed(() => parseShareQuery(route.query as Record<string, unk
|
||||
const shareTitle = computed(() => {
|
||||
const p = payload.value
|
||||
if (!p) return '分享'
|
||||
return p.type === 'relation' ? '关系理解分享' : '个人画像分享'
|
||||
if (p.type === 'relation') return '关系理解分享'
|
||||
if (p.type === 'star') return '星座分享'
|
||||
return '个人画像分享'
|
||||
})
|
||||
const shareSub = computed(() => {
|
||||
const p = payload.value
|
||||
if (!p) return ''
|
||||
return p.type === 'relation' ? '朋友分享了一段关系理解' : '朋友分享了一份个人画像'
|
||||
if (p.type === 'relation') return '朋友分享了一段关系理解'
|
||||
if (p.type === 'star') return '朋友分享了一份星座探索'
|
||||
return '朋友分享了一份个人画像'
|
||||
})
|
||||
|
||||
const heroIcon = computed(() => {
|
||||
const t = payload.value?.type
|
||||
if (t === 'relation') return 'relation'
|
||||
if (t === 'star') return 'star'
|
||||
return 'portrait'
|
||||
})
|
||||
|
||||
const cardProps = computed(() => {
|
||||
const p = payload.value
|
||||
if (!p) return {}
|
||||
if (p.type === 'portrait') {
|
||||
if (p.type === 'portrait' || p.type === 'star') {
|
||||
return {
|
||||
type: 'portrait' as const,
|
||||
type: p.type,
|
||||
title: p.title,
|
||||
line: p.line,
|
||||
keywords: p.keywords,
|
||||
ctaLabel: '查看完整成长报告',
|
||||
ctaLabel: p.type === 'star' ? '查看完整星座报告' : '查看完整成长报告',
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -81,10 +92,18 @@ const cardProps = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const ctaTo = computed(() => (payload.value?.type === 'relation' ? '/relation' : '/'))
|
||||
const ctaText = computed(() =>
|
||||
payload.value?.type === 'relation' ? '开始关系理解' : '生成我的个人画像',
|
||||
)
|
||||
const ctaTo = computed(() => {
|
||||
const t = payload.value?.type
|
||||
if (t === 'relation') return '/relation'
|
||||
if (t === 'star') return '/star'
|
||||
return '/'
|
||||
})
|
||||
const ctaText = computed(() => {
|
||||
const t = payload.value?.type
|
||||
if (t === 'relation') return '开始关系理解'
|
||||
if (t === 'star') return '生成我的星座'
|
||||
return '生成我的个人画像'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -4,8 +4,11 @@ import StarProfilePage from './StarProfilePage.vue'
|
||||
|
||||
const { api, routeQuery } = vi.hoisted(() => ({
|
||||
api: {
|
||||
authMe: vi.fn(),
|
||||
listProfiles: vi.fn(),
|
||||
createProfile: vi.fn(),
|
||||
createStar: vi.fn(),
|
||||
getLatestReport: vi.fn(),
|
||||
getReport: vi.fn(),
|
||||
createOrder: vi.fn(),
|
||||
payMock: vi.fn(),
|
||||
@@ -15,8 +18,8 @@ const { api, routeQuery } = vi.hoisted(() => ({
|
||||
|
||||
vi.mock('../api/client', () => ({ api }))
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({ query: routeQuery }),
|
||||
useRouter: () => ({ back: vi.fn() }),
|
||||
useRoute: () => ({ query: routeQuery, fullPath: '/star?y=1990&m=5&d=12' }),
|
||||
useRouter: () => ({ back: vi.fn(), replace: vi.fn(), push: vi.fn() }),
|
||||
}))
|
||||
|
||||
const summary = {
|
||||
@@ -39,22 +42,29 @@ const summary = {
|
||||
keywords: ['金牛'],
|
||||
}
|
||||
|
||||
const starReport = {
|
||||
id: 'r1',
|
||||
type: 'star',
|
||||
has_deep_access: false,
|
||||
summary,
|
||||
detail: null,
|
||||
}
|
||||
|
||||
describe('StarProfilePage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
api.authMe.mockResolvedValue({ id: 'u1' })
|
||||
api.listProfiles.mockResolvedValue({ items: [] })
|
||||
api.createProfile.mockResolvedValue({ id: 'p1' })
|
||||
api.createStar.mockResolvedValue({
|
||||
id: 'r1',
|
||||
has_deep_access: false,
|
||||
summary,
|
||||
detail: null,
|
||||
})
|
||||
api.getLatestReport.mockResolvedValue(starReport)
|
||||
api.createStar.mockResolvedValue(starReport)
|
||||
})
|
||||
|
||||
it('shows natal wheel and free chart content', async () => {
|
||||
const w = mount(StarProfilePage, { global: { stubs: { RouterLink: true } } })
|
||||
await flushPromises()
|
||||
expect(api.createStar).toHaveBeenCalled()
|
||||
expect(api.createProfile).toHaveBeenCalled()
|
||||
expect(api.getLatestReport).toHaveBeenCalledWith('p1', 'star')
|
||||
await w.findAll('button').find((b) => b.text() === '星盘')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(w.find('svg.wheel').exists()).toBe(true)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# Backend Design: ECR-012 星座对齐收口
|
||||
|
||||
> Architect 产出;Engineer 只消费本文件。
|
||||
> 实现纪律:`$ESS_ROOT/technology-governance/BACKEND_STANDARD.md`
|
||||
> 项目分层权威:`.ai/architecture.md`
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| ID | BD-2026-012 |
|
||||
| ECR | ECR-012 |
|
||||
| Change Level | L2 |
|
||||
| Status | Approved |
|
||||
| Approved | 2026-08-07(Human) |
|
||||
| Author | Architect |
|
||||
| Date | 2026-08-07 |
|
||||
| Risk | Low |
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
- 问题 / 目标:星座/合盘主路径已落地,需 Spec 对齐、H5/报告页一致、测试加厚;默认 **不改表结构**。
|
||||
- 非目标:真支付、新模块、换星历、Ops。
|
||||
- 相关 Spec:`star-profile.md`;前序 ECR-003(fortune→outlook)。
|
||||
|
||||
## Architecture Change
|
||||
|
||||
- 是否改变分层边界:**No**
|
||||
- 受影响层:Application(`internal/star` 仅按需)· API 文档 · UI(user-h5)
|
||||
|
||||
## Module Changes
|
||||
|
||||
| Module | Layer | Change | Must NOT |
|
||||
|--------|-------|--------|----------|
|
||||
| `internal/star` / `outlook` / `synastry` | Domain/App | 仅修偏离 Spec 的 summary/detail 键或文案 | 新表;换 ephemeris |
|
||||
| `handler` reports/synastry | API | 契约不变则不动;OpenAPI 描述可补 | Handler SQL |
|
||||
| `user-h5` star/synastry/report | UI | 对齐面板、卫生、分享、测试 | 直连 DB;真支付 |
|
||||
| packages/types · sdk | Shared | 仅当 OpenAPI/DTO 需同步 | 发明新领域同义词 |
|
||||
|
||||
## Data Flow
|
||||
|
||||
```text
|
||||
Profile(birth*)
|
||||
→ POST /reports/star | /reports/synastry
|
||||
→ star engine / synastry.BuildReport
|
||||
→ GrowthReport summary(outlook, charts, …) + gated detail
|
||||
→ H5 StarProfilePage | Synastry* | ReportPage
|
||||
→(可选)pay-mock deep_access → detail 可见
|
||||
```
|
||||
|
||||
## API Changes
|
||||
|
||||
- 契约:`proto/openapi.yaml`
|
||||
- 变更:**默认 None**(路径与语义保持);允许描述/`$ref` 补强
|
||||
- 兼容:禁止重新引入 summary.`fortune`
|
||||
|
||||
## Database Changes
|
||||
|
||||
- 表 / 字段:**无**
|
||||
- Migration Required:**NO**
|
||||
|
||||
## Migration Plan
|
||||
|
||||
- Required:NO
|
||||
- 回滚:代码回滚即可
|
||||
|
||||
## Failure Handling
|
||||
|
||||
- 无档案 / 缺生日:Empty 表单(现有)
|
||||
- detail 无权益:服务端剥离(现有,须保持测试)
|
||||
- 旧报告缺 outlook:H5 空态,不伪造 fortune
|
||||
|
||||
## Test Plan
|
||||
|
||||
- Unit:outlook bundle keys;H5 fortuneBundleFrom
|
||||
- Integration:`TestFlowStarDeepAccess` · synastry charts keys · 禁用词
|
||||
- E2E:p2 star 路径 mock 含 outlook
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
- git revert 本 ECR;无数据迁移
|
||||
|
||||
---
|
||||
|
||||
## Backend Change Boundary
|
||||
|
||||
```text
|
||||
Change Level: L2
|
||||
Change: star/synastry Spec alignment (mostly H5 + tests; optional star package hygiene)
|
||||
|
||||
Affected:
|
||||
Domain: StarProfile / Synastry (existing)
|
||||
Application: internal/star* (optional)
|
||||
Infrastructure: none (no migration)
|
||||
API: OpenAPI docs only (default)
|
||||
Migration: Not required
|
||||
Tests: integration + H5 + e2e
|
||||
Risk: Low
|
||||
```
|
||||
|
||||
## Architecture Regression Check
|
||||
|
||||
- [ ] 无 Handler 直连 DB
|
||||
- [ ] 不改鉴权面
|
||||
- [ ] 未引入非默认中间件 / 新星历依赖
|
||||
- [ ] 未简化分层
|
||||
- [ ] 无 summary.`fortune` 回归
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
## 2026-08-07
|
||||
|
||||
- **ECR-012 Implemented**:星座对齐收口(outlook 卫生 · 报告页复用运势面板 · 分享 type=star · 测试加厚)
|
||||
TEST_REPORT PASS · STATE phase=review · 待 Reviewer Closed
|
||||
- **ECR-012 Approved**(Human):星座对齐进入 implement
|
||||
BD-2026-012 Approved · TASK ready · STATE owner=engineer
|
||||
- **ECR-012 Draft**:星座对齐收口(星盘 · outlook · 合盘)
|
||||
工件:PRODUCT/ENGINEERING/BACKEND_DESIGN · HANDOFF · TASK · STATE
|
||||
Gap G1–G6;**真支付排除**
|
||||
- **ECR-007 Closed** · **ECR-008 Closed**(Reviewer):review 门禁 PASS · STATE done · TRACEABILITY 更新
|
||||
- **ECR-008 P1 修复**:审计同事务 · 宫格 move 保留 row_index · Analytics nil 守卫
|
||||
- **ECR-008 Implemented**:Ops-C 首页宫格 CMS + 测评上下架
|
||||
migration `000014` · `/home/tools` · admin「内容」· TEST_REPORT/CODE_REVIEW
|
||||
|
||||
@@ -24,4 +24,13 @@ Date: 2026-08-07
|
||||
|
||||
- ingest 逐条 INSERT(量上来再批量 COPY / 日汇总表)
|
||||
- admin「数据」未进 `@yuxingu/sdk`(与 Ops-A 一致用 thin client)
|
||||
- Ops-C/D/E 另开 ECR
|
||||
- Ops-D/E 另开 ECR
|
||||
|
||||
## Closed decision(2026-08-07)
|
||||
|
||||
```text
|
||||
Load Agent Profile: REVIEWER
|
||||
Decision: Approve → Closed
|
||||
Evidence: TEST_REPORT/ECR-007.md · Acceptance 全勾 · ess-validate --phase review PASS
|
||||
Residual: 上列不阻断;部署属 RELEASE_MANAGER + Human
|
||||
```
|
||||
|
||||
@@ -32,3 +32,12 @@ Date: 2026-08-07 · Reviewer pass → P1 fixed
|
||||
- [x] Admin / Device 鉴权隔离
|
||||
- [x] 未引入非默认中间件
|
||||
- [x] 审计与写操作同事务
|
||||
|
||||
## Closed decision(2026-08-07)
|
||||
|
||||
```text
|
||||
Load Agent Profile: REVIEWER
|
||||
Decision: Approve → Closed
|
||||
Evidence: TEST_REPORT/ECR-008.md · P1 已修 · Acceptance 全勾 · ess-validate --phase review PASS
|
||||
Residual: P2 可跟进项不阻断 Closed;部署属 RELEASE_MANAGER + Human
|
||||
```
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# ECR-007
|
||||
|
||||
**Title:** 运营行为分析 Ops-B(自有埋点采集 · 管理端数据看板)
|
||||
**Status:** Approved
|
||||
**Status:** Closed
|
||||
**Date:** 2026-08-07
|
||||
**Approved:** 2026-08-07(Human)
|
||||
**Closed:** 2026-08-07(Reviewer · Approve + ess-validate review PASS)
|
||||
**Change Level:** L2(新能力;栈与分层不变)
|
||||
|
||||
## Change
|
||||
@@ -50,8 +51,9 @@
|
||||
- [x] Integration:H5 形态上报 → DB → admin analytics GET 非空且量级一致
|
||||
- [x] `go test` 相关包绿;`npm run build:h5` · `build:admin` 绿
|
||||
- [x] OpenAPI / TRACEABILITY / STATE / TEST_REPORT / CODE_REVIEW
|
||||
- [ ] `python scripts/ess-validate.py --phase review --ecr ECR-007`(Reviewer 门禁)
|
||||
- [x] `python3 scripts/ess-validate.py --phase review --ecr ECR-007`(Reviewer 门禁 PASS)
|
||||
- [x] Docs + CHANGELOG
|
||||
- [x] Reviewer Closed(STATE · TRACEABILITY)
|
||||
|
||||
## Rollback
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# ECR-008
|
||||
|
||||
**Title:** 运营内容配置 Ops-C(首页宫格 CRUD · 测评上下架)
|
||||
**Status:** Approved
|
||||
**Status:** Closed
|
||||
**Date:** 2026-08-07
|
||||
**Approved:** 2026-08-07(Human)
|
||||
**Closed:** 2026-08-07(Reviewer · Approve + ess-validate review PASS)
|
||||
**Change Level:** L2(新能力;栈与分层不变)
|
||||
|
||||
## Change
|
||||
@@ -48,8 +49,9 @@
|
||||
- [x] Integration:PUT tools → GET home/tools;PATCH scale draft → ListPublished 不含
|
||||
- [x] `go test` · `build:h5` · `build:admin` 绿
|
||||
- [x] OpenAPI / TRACEABILITY / STATE / TEST_REPORT / CODE_REVIEW
|
||||
- [x] `ess-validate --phase review`
|
||||
- [x] `python3 scripts/ess-validate.py --phase review --ecr ECR-008`(PASS)
|
||||
- [x] Docs + CHANGELOG
|
||||
- [x] Reviewer Closed(STATE · TRACEABILITY)
|
||||
|
||||
## Rollback
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# ECR-012
|
||||
|
||||
**Title:** 星座对齐收口(星盘 · 运势 outlook · 合盘)
|
||||
**Status:** Approved
|
||||
**Date:** 2026-08-07
|
||||
**Approved:** 2026-08-07(Human)
|
||||
**Change Level: L2**(可见体验/契约文档对齐;栈与分层不变)
|
||||
|
||||
## Change
|
||||
|
||||
在已有 P2 星座/合盘实现上做 **Spec ↔ 实现对齐收口**(非真支付、非新探索模块):
|
||||
|
||||
1. 修订 Active Feature Spec:`.ai/product/feature-spec/star-profile.md`(Gaps · §12 用词 · Implementation Notes)
|
||||
2. H5:`/star` · `/reports/:id`(type=star) · `/synastry*` 体验与文案对齐 Spec §5–12
|
||||
3. 清理 ECR-003 残留:类型/组件内 `lucky` 字段、内部仍混用的 `fortune` JSON 读路径(**用户可见 Tab 文案「运势」保留**)
|
||||
4. 报告页星座面板与星座主页运势信息密度对齐(dims / boost / caution / 日–一生)
|
||||
5. 补强测试:integration/H5/e2e 对 outlook · 轮盘 · 合盘五主盘键的断言;lexicon 禁用词扫描覆盖合盘路径
|
||||
6. 更新 `feature-map` / `P2-BACKLOG`:星座由「对齐中」→ 本 ECR Closed 后标 Done
|
||||
|
||||
## Motivation
|
||||
|
||||
P2-BACKLOG 已写「星盘/运势/合盘对齐中」:主路径可跑,但 Spec 验收措辞、报告页与主页体验、H5 命名卫生与 e2e mock 偏薄,导致无法宣称星座模块 Feature Complete。
|
||||
|
||||
## Scope
|
||||
|
||||
### Allowed
|
||||
|
||||
- 上列 Spec / H5 /(必要时)`internal/star` 文案或 summary 键卫生 · OpenAPI 描述补强
|
||||
- 埋点:保持 `portrait_completed{source:star}` 与 Spec「`star_completed` / portrait_completed source=star」等价;若补 `star_completed` 别名须同步 analytics 白名单
|
||||
- 分享:星座结果分享 payload 类型/标题与 lexicon 一致(若现用 `portrait` 类型属偏差则修正)
|
||||
- TRACEABILITY · STATE · TEST_REPORT · CODE_REVIEW · CHANGELOG
|
||||
|
||||
### Forbidden(Non-goals)
|
||||
|
||||
- **真支付网关**(支付仍 mock;另开 ECR,排期最后)
|
||||
- 微信 OAuth / 短信 OTP · 原生分享 SDK · 短链服务
|
||||
- 达人 / 消息 / UGC / 独立运势 Feed
|
||||
- 换星历引擎、改宫位制(Whole Sign 冻结)、新表 migration(本 ECR **默认无 migration**)
|
||||
- 身心节律 / 意象卡片 / Ops-D+ / 小程序业务
|
||||
|
||||
## Risk
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| 改 Tab 内部 key 破坏深链 | 用户可见「运势」不变;URL/query 变更须兼容或写清迁移 |
|
||||
| 过度改 JSON 破坏旧报告 | 只读 `outlook`;禁止写回 `fortune`;旧库报告无 outlook 时 H5 降级空态 |
|
||||
| 文案踩 lexicon | 合盘免责已有;扫「占卜/算命」恐吓;允许运势分与匹配指数 |
|
||||
| 范围膨胀成内容大修 | 深度版长文「明显缺段」才补;不重写整包星盘文案库 |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Spec `star-profile.md` §12 用例可映射到测试;§14 Gaps 反映本 ECR 收口结果
|
||||
- [x] `/star`:Empty 生日表单 → Normal(轮盘 + outlook 日/周/月/年/一生 + 行运)+ Locked 深度墙;无禁用恐吓词
|
||||
- [x] `/reports/:id` type=star:运势面板信息密度不低于主页基础层(score/label/tip + dims 或显式降级说明)
|
||||
- [x] `/synastry`:三指数 + `charts` 五主盘 + 三推运 key 可见(无权益时 detail 剥离)
|
||||
- [x] JSON summary **无** `fortune` 键;H5 **不读** `lucky`
|
||||
- [x] Integration:`TestFlowStarDeepAccess` + synastry charts 键保持绿;必要时增补断言
|
||||
- [x] H5 单测 / e2e(p2 star 路径)mock 含 outlook 最小字段
|
||||
- [x] OpenAPI / TRACEABILITY / STATE / TEST_REPORT(CODE_REVIEW 待 Reviewer)
|
||||
- [ ] `python3 scripts/ess-validate.py --phase review --ecr ECR-012`(Reviewer)
|
||||
- [x] Docs + CHANGELOG;P2-BACKLOG 星座行更新
|
||||
|
||||
## Rollback
|
||||
|
||||
- 回滚本 ECR 提交;不涉及 DB down migration
|
||||
- 若仅文档 Closed 前中止:将 STATE 标 Rejected / 搁置
|
||||
|
||||
## Risk Review
|
||||
|
||||
- Path: N/A(非交易)
|
||||
|
||||
## Linked
|
||||
|
||||
- Feature Spec: `.ai/product/feature-spec/star-profile.md`
|
||||
- PRODUCT_SPEC: `docs/PRODUCT_SPEC/ECR-012-star-alignment.md`
|
||||
- ENGINEERING_SPEC: `docs/ENGINEERING_SPEC/ECR-012-star-alignment.md`
|
||||
- BACKEND_DESIGN: `docs/BACKEND_DESIGN/BD-2026-012-star-alignment.md`(BD-2026-012)
|
||||
- HANDOFF / TASK / STATE:同号前缀
|
||||
- 前序:ECR-003 outlook 卫生 · ECR-004/005 拆分 · P2 星座已落地
|
||||
- 明确后置:真支付 ECR(最后)
|
||||
@@ -0,0 +1,28 @@
|
||||
# ENGINEERING_SPEC — ECR-012 星座对齐收口
|
||||
|
||||
权威设计:`docs/BACKEND_DESIGN/BD-2026-012-star-alignment.md`
|
||||
产品:`.ai/product/feature-spec/star-profile.md` · `docs/PRODUCT_SPEC/ECR-012-star-alignment.md`
|
||||
|
||||
## Implement order
|
||||
|
||||
1. **Spec 修订**(可与代码同 PR,但语义先冻结 Gaps):`star-profile.md` §12 措辞 · §14 Gaps · 指向 ECR-012
|
||||
2. **盘点确认 G5**:分享类型;确认后改 `useStarProfilePage` sharePayload
|
||||
3. **H5 卫生**:去掉对 `lucky` 的类型依赖;`fortuneBundleFrom` 仅读 `outlook`;组件文件名可保留或渐进改名(不必强行大挪文件)
|
||||
4. **报告页对齐**:`ReportStarPanel` 运势展示对齐主页字段(可抽共享小组件,注意 ≤400 行/文件)
|
||||
5. **(按需)后端**:仅当 summary 缺键或 detail 裁剪偏离 Spec 时改 `internal/star`;**默认无 migration**
|
||||
6. **OpenAPI**:补强 star/synastry 响应描述或 `$ref`(若现偏薄)
|
||||
7. **测试**:integration 断言保持;H5 spec;e2e mock 补 outlook 最小集;禁用词扫描覆盖合盘
|
||||
8. **文档收口**:P2-BACKLOG · feature-map · TRACEABILITY · STATE · CHANGELOG
|
||||
|
||||
## Constraints
|
||||
|
||||
- 函数 ≤50 行 · 文件 ≤400 行
|
||||
- UI → `@yuxingu/sdk` → Handler → Service → Repository
|
||||
- 不引入新星历依赖;不改 Whole Sign / 热带设定(无 ADR)
|
||||
- 不接真支付;深度版继续 pay-mock
|
||||
- 不提交 `config.local.yaml` / 密钥
|
||||
- lexicon:禁止占卜/算命恐吓;允许运势分与合盘指数
|
||||
|
||||
## Done when
|
||||
|
||||
ECR Acceptance 全勾 + `.ai/definition-of-done.md` Review Report + `ess-validate --phase review`(Approved 实现后)
|
||||
@@ -16,3 +16,7 @@
|
||||
## Ask
|
||||
|
||||
Reviewer 勾选 Approve;可将 ECR-007 标 Closed。
|
||||
|
||||
## Closed
|
||||
|
||||
2026-08-07 Reviewer:**Approve → Closed**(STATE · TRACEABILITY · ECR Status)。
|
||||
|
||||
@@ -15,3 +15,7 @@
|
||||
## Ask
|
||||
|
||||
Reviewer Approve;可将 ECR-008 标 Closed。
|
||||
|
||||
## Closed
|
||||
|
||||
2026-08-07 Reviewer:**Approve → Closed**(STATE · TRACEABILITY · ECR Status)。
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# HANDOFF — ECR-012 Architect → Engineer
|
||||
|
||||
## Gate
|
||||
|
||||
**仅当**下列均为 **Approved** 后,方可改 `apps/` / `packages/`:
|
||||
|
||||
1. `docs/ECR/ECR-012-star-alignment.md`
|
||||
2. `docs/BACKEND_DESIGN/BD-2026-012-star-alignment.md`(BD-2026-012)
|
||||
|
||||
**当前(2026-08-07):二者均为 Approved(Human)→ Engineer 可改 `apps/`。**
|
||||
|
||||
## Consume
|
||||
|
||||
1. `.ai/product/feature-spec/star-profile.md`
|
||||
2. `docs/ECR/ECR-012-star-alignment.md`
|
||||
3. `docs/PRODUCT_SPEC/ECR-012-star-alignment.md`(含 Gap G1–G6)
|
||||
4. `docs/BACKEND_DESIGN/BD-2026-012-star-alignment.md`
|
||||
5. `docs/ENGINEERING_SPEC/ECR-012-star-alignment.md`
|
||||
6. `$ESS_ROOT/technology-governance/BACKEND_STANDARD.md`
|
||||
7. 本仓:`docs/PROJECT_RULES.md` · `.ai/architecture.md` · `.ai/product/lexicon.md`
|
||||
|
||||
## Do
|
||||
|
||||
按 ENGINEERING_SPEC Implement order 收口星座对齐(G1–G6)。
|
||||
|
||||
## Do not
|
||||
|
||||
- 真支付 / 换支付适配器
|
||||
- 新 migration(除非实现中发现必须,则回 Architect 升边界)
|
||||
- 节律 / 意象卡片 / Ops-D+ / 小程序
|
||||
- Handler → DB
|
||||
- 重新引入 summary.`fortune`
|
||||
|
||||
## Return
|
||||
|
||||
1. `docs/HANDOFF/ECR-012-engineer-to-reviewer.md`
|
||||
2. `docs/TEST_REPORT/ECR-012.md`
|
||||
3. `docs/CODE_REVIEW/ECR-012.md`(Reviewer 填)
|
||||
4. 更新 `docs/STATE/ECR-012.md` owner → reviewer
|
||||
5. `python3 scripts/ess-validate.py --phase review --ecr ECR-012`
|
||||
@@ -0,0 +1,19 @@
|
||||
# HANDOFF — ECR-012 Engineer → Reviewer
|
||||
|
||||
## Done
|
||||
|
||||
- H5:`fortuneBundleFrom` 仅映射 outlook 周期键并剥离 `lucky`
|
||||
- `ReportStarPanel` 复用 `StarFortunePanel`(dims/boost/caution 对齐主页)
|
||||
- 分享:`type=star`(shareLink / SharePage / ShareSheet / 星座页)
|
||||
- 单测与 e2e mock 加厚;integration 断言无 `lucky`
|
||||
- OpenAPI star 响应描述补强;Spec Gaps 更新
|
||||
- `docs/TEST_REPORT/ECR-012.md`
|
||||
|
||||
## Evidence
|
||||
|
||||
`docs/TEST_REPORT/ECR-012.md` · Acceptance 见 ECR-012
|
||||
|
||||
## Ask
|
||||
|
||||
Reviewer 勾选 Approve;通过后可将 ECR-012 标 Closed(P2-BACKLOG 去掉「对齐中」)。
|
||||
真支付仍后置。
|
||||
@@ -0,0 +1,44 @@
|
||||
# PRODUCT_SPEC — ECR-012 星座对齐收口
|
||||
|
||||
对齐 Feature Spec:`.ai/product/feature-spec/star-profile.md`
|
||||
|
||||
## Outcome
|
||||
|
||||
1. 用户在 `/star` 完成「填生日 → 看星盘/运势/行星 → 深度墙」路径,体验与 Spec §4–6 一致。
|
||||
2. 合盘 `/synastry` 五主盘 + 次限推运 + 三指数与 Spec 一致;邀请/附近不回退。
|
||||
3. 成长报告详情中 `type=star` 的运势展示与主路径对齐,避免「主页有、报告页空」。
|
||||
4. 工程与文档不再出现已废弃的 summary.`fortune` / `lucky` 读路径。
|
||||
5. P2-BACKLOG 可在 Closed 后去掉「对齐中」措辞。
|
||||
|
||||
## Actors
|
||||
|
||||
| Actor | 能力(本 ECR 后) |
|
||||
|-------|-------------------|
|
||||
| Visitor + 档案 | 基础星盘 · 日–一生运势摘要 · 行运 · 合盘指数与盘缩略 |
|
||||
| DeepAccess / VIP | detail:完整相位 · 年/一生深析 · 合盘相处/推运深文案 |
|
||||
| 无档案 | `/star` Empty 页内生日(可选时/地) |
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- Spec §12 五条 Given/When/Then 均有自动化或明确手工证据
|
||||
- e2e / 单测失败率不因本 ECR 回退
|
||||
- lexicon 硬禁止词不出现在星座/合盘主路径 UI 与 API 样例
|
||||
|
||||
## Out of scope
|
||||
|
||||
见 ECR-012 Forbidden;真支付、登录增强、节律/卡片、Ops-D+ 另开。
|
||||
|
||||
## Gap 基线(Architect 盘点 · 2026-08-07)
|
||||
|
||||
| ID | Gap | 建议级别 |
|
||||
|----|-----|----------|
|
||||
| G1 | Spec §12 / 注释仍写 fortune;缺 Gaps 栏 | **Done**(本 ECR) |
|
||||
| G2 | H5 组件/类型残留 `lucky`;内部 `fortune` 命名与 JSON `outlook` 混用 | **Done** |
|
||||
| G3 | `ReportStarPanel` 运势层薄于 `StarFortunePanel` | **Done**(复用面板) |
|
||||
| G4 | e2e star mock 过瘦(无 outlook/planets) | **Done** |
|
||||
| G5 | 分享 payload 可能仍标 `portrait` | **Done**(type=star) |
|
||||
| G6 | feature-map / P2-BACKLOG「对齐中」状态未闭环 | **Pending Closed** |
|
||||
|
||||
## 附录:排期约束
|
||||
|
||||
- **真支付最后做**(产品指令);本 ECR 继续 pay-mock。
|
||||
@@ -46,9 +46,9 @@
|
||||
|
||||
## Active anchors
|
||||
|
||||
- ECR: **ECR-006** 运营后台 Phase A(Closed);ECR-001–005 Closed / shipped
|
||||
- ECR: **ECR-012** 星座对齐(**Implemented** · review);ECR-006~008 Ops A/B/C Closed;ECR-001–005 Closed
|
||||
- EXP: (无)
|
||||
- STATE: `docs/STATE/ECR-006.md`
|
||||
- STATE: `docs/STATE/ECR-012.md`(owner=reviewer)
|
||||
- TRACEABILITY: `docs/TRACEABILITY.md`
|
||||
- ADR: `.ai/adr/0007-ess-ai-dual-track.md`
|
||||
- Product status: `.ai/product/p1-status.md`(**P1 Complete**)
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
|-------|-------|
|
||||
| ECR | ECR-007 |
|
||||
| Title | 运营行为分析 Ops-B |
|
||||
| Status | Implemented(待 Reviewer Closed) |
|
||||
| Phase | review |
|
||||
| Owner | reviewer |
|
||||
| Status | **Closed** |
|
||||
| Phase | done |
|
||||
| Owner | — |
|
||||
| Spec | `.ai/product/feature-spec/ops-analytics.md`(Active) |
|
||||
| Backend Design | BD-2026-007 Approved |
|
||||
| Test | `docs/TEST_REPORT/ECR-007.md` PASS |
|
||||
| Review | `docs/CODE_REVIEW/ECR-007.md` Approve |
|
||||
| Next | Human/Reviewer Closed ECR · 部署 API + 静态资源 |
|
||||
| Review | `docs/CODE_REVIEW/ECR-007.md` Approve → Closed |
|
||||
| Updated | 2026-08-07 |
|
||||
|
||||
Ops-B shipped: analytics ingest + admin「数据」看板.
|
||||
部署/Promote 由 RELEASE_MANAGER + Human 另办(不挡 Closed)。
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
|-------|-------|
|
||||
| ECR | ECR-008 |
|
||||
| Title | 运营内容配置 Ops-C |
|
||||
| Status | Implemented(待 Reviewer Closed) |
|
||||
| Phase | review |
|
||||
| Owner | reviewer |
|
||||
| Spec | Active |
|
||||
| Status | **Closed** |
|
||||
| Phase | done |
|
||||
| Owner | — |
|
||||
| Spec | `.ai/product/feature-spec/ops-content.md`(Active) |
|
||||
| Backend Design | BD-2026-008 Approved |
|
||||
| Test | TEST_REPORT/ECR-008.md PASS |
|
||||
| Review | CODE_REVIEW/ECR-008.md Approve |
|
||||
| Test | `docs/TEST_REPORT/ECR-008.md` PASS |
|
||||
| Review | `docs/CODE_REVIEW/ECR-008.md` Approve → Closed |
|
||||
| Updated | 2026-08-07 |
|
||||
|
||||
Ops-C shipped: 首页宫格 CMS + 测评上下架.
|
||||
部署/Promote 由 RELEASE_MANAGER + Human 另办(不挡 Closed)。
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# STATE — ECR-012
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| ECR | ECR-012 |
|
||||
| Title | 星座对齐收口(星盘 · 运势 outlook · 合盘) |
|
||||
| Status | Implemented(待 Reviewer Closed) |
|
||||
| Phase | review |
|
||||
| Owner | reviewer |
|
||||
| Spec | `.ai/product/feature-spec/star-profile.md` |
|
||||
| Backend Design | BD-2026-012 Approved |
|
||||
| Test | `docs/TEST_REPORT/ECR-012.md` PASS |
|
||||
| Review | —(待 `docs/CODE_REVIEW/ECR-012.md`) |
|
||||
| Updated | 2026-08-07 |
|
||||
|
||||
Engineer 已交 HANDOFF → Reviewer。
|
||||
Constraint: 真支付不在本 ECR。
|
||||
@@ -1,8 +1,8 @@
|
||||
id: TASK-007-ECR007
|
||||
ecr: ECR-007
|
||||
title: Ops-B analytics ingest + admin data dashboard
|
||||
role: engineer
|
||||
status: review
|
||||
role: reviewer
|
||||
status: done
|
||||
change_level: L2
|
||||
inputs:
|
||||
- docs/HANDOFF/ECR-007-architect-to-engineer.md
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
id: TASK-008-ECR008
|
||||
ecr: ECR-008
|
||||
title: Ops-C home tools CMS + scales publish
|
||||
role: engineer
|
||||
status: review
|
||||
role: reviewer
|
||||
status: done
|
||||
change_level: L2
|
||||
inputs:
|
||||
- docs/HANDOFF/ECR-008-architect-to-engineer.md
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
id: TASK-012-ECR012
|
||||
ecr: ECR-012
|
||||
title: Star/synastry Spec alignment (outlook · report parity · tests)
|
||||
role: reviewer
|
||||
status: review
|
||||
change_level: L2
|
||||
inputs:
|
||||
- docs/HANDOFF/ECR-012-engineer-to-reviewer.md
|
||||
- docs/TEST_REPORT/ECR-012.md
|
||||
- docs/BACKEND_DESIGN/BD-2026-012-star-alignment.md
|
||||
- docs/ENGINEERING_SPEC/ECR-012-star-alignment.md
|
||||
- .ai/product/feature-spec/star-profile.md
|
||||
outputs:
|
||||
- docs/CODE_REVIEW/ECR-012.md
|
||||
acceptance:
|
||||
- ECR-012 Acceptance 全勾(review 门禁除外)
|
||||
- 无 summary.fortune / lucky 回归
|
||||
- 真支付不在范围
|
||||
@@ -0,0 +1,40 @@
|
||||
# TEST_REPORT — ECR-012 星座对齐收口
|
||||
|
||||
Date: 2026-08-07
|
||||
ECR: ECR-012 · BD-2026-012
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.local/go/bin:$PATH"
|
||||
cd apps/api && go test ./internal/integration/ -run TestFlowStarDeepAccess -count=1
|
||||
cd ../.. && npm run test:h5 -- --run src/lib/shareLink.spec.ts src/pages/StarProfilePage.spec.ts
|
||||
npm run build:h5
|
||||
# optional e2e: npm run test:e2e -- e2e/p2-explore-paths.spec.ts
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| `TestFlowStarDeepAccess`(outlook · 无 fortune · 无 lucky) | PASS |
|
||||
| `shareLink.spec.ts`(含 star round-trip) | PASS |
|
||||
| `StarProfilePage.spec.ts`(轮盘 · 今日/一生运势) | PASS |
|
||||
| `build:h5` | PASS |
|
||||
|
||||
## Spec / Gap mapping
|
||||
|
||||
| Gap | Evidence |
|
||||
|-----|----------|
|
||||
| G1 Spec §12 outlook 措辞 + Gaps | `star-profile.md` |
|
||||
| G2 剥离 lucky;fortuneBundle 仅读 outlook 周期键 | `reportSummary.fortuneBundleFrom` · StarFortunePanel 无 lucky |
|
||||
| G3 报告页运势密度 | `ReportStarPanel` 复用 `StarFortunePanel` |
|
||||
| G4 e2e mock 含 outlook | `e2e/p2-explore-paths.spec.ts` |
|
||||
| G5 分享 type=star | `shareLink` · SharePage · ShareSheet · useStarProfilePage |
|
||||
| G6 Backlog 状态 | P2-BACKLOG → Implemented(待 Reviewer Closed) |
|
||||
|
||||
## Notes
|
||||
|
||||
- 真支付未引入;深度墙仍 pay-mock
|
||||
- UI Tab 文案仍为「运势」;JSON 契约为 `outlook`
|
||||
- commit sha:实现提交后写入 TRACEABILITY
|
||||
@@ -12,5 +12,6 @@
|
||||
| ECR-004 | Synastry 再拆 · Scale 测 · Membership 守卫 · OpenAPI · CI | review PASS | TEST_REPORT · CODE_REVIEW/ECR-004.md |
|
||||
| ECR-005 | Ask/Decode/Portrait 再拆 · composable 外提 · OpenAPI `$ref` | review PASS | TEST_REPORT · CODE_REVIEW/ECR-005.md |
|
||||
| ECR-006 | 运营后台 Phase A(admin API + admin-h5) | **Closed** | Spec ops-admin · BD-2026-006 · TEST_REPORT · CODE_REVIEW |
|
||||
| ECR-007 | 运营行为分析 Ops-B(埋点 + 数据看板) | **Implemented** | Spec Active · BD-2026-007 · TEST_REPORT · CODE_REVIEW · HANDOFF review |
|
||||
| ECR-008 | 运营内容 Ops-C(首页宫格 + 测评上下架) | **Implemented** | Spec Active · BD-2026-008 · TEST_REPORT · CODE_REVIEW |
|
||||
| ECR-007 | 运营行为分析 Ops-B(埋点 + 数据看板) | **Closed** | Spec Active · BD-2026-007 · TEST_REPORT · CODE_REVIEW · STATE |
|
||||
| ECR-008 | 运营内容 Ops-C(首页宫格 + 测评上下架) | **Closed** | Spec Active · BD-2026-008 · TEST_REPORT · CODE_REVIEW · STATE |
|
||||
| ECR-012 | 星座对齐收口(星盘 · outlook · 合盘) | **Implemented** | Spec star-profile · BD-2026-012 · TEST_REPORT · HANDOFF review |
|
||||
|
||||
+1
-1
@@ -475,7 +475,7 @@ paths:
|
||||
profile_id: { type: string, format: uuid }
|
||||
responses:
|
||||
'200':
|
||||
description: GrowthReport type=star(含 chart/planets/aspects_preview/outlook/transits)
|
||||
description: GrowthReport type=star;summary 含 chart/planets/aspects_preview/outlook(日周月年一生)/transits;无 fortune/lucky 键;detail 按权益裁剪
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
||||
Reference in New Issue
Block a user