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:
jackyu66git
2026-08-07 15:48:19 +08:00
co-authored by Cursor
parent 7ab9add5dd
commit d33c8fdfe9
42 changed files with 658 additions and 129 deletions
+7 -7
View File
@@ -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 文案)。
+3 -2
View File
@@ -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 |
+3 -3
View File
@@ -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` → 填满 → 在本表登记 → 再编码。
+2 -2
View File
@@ -206,7 +206,7 @@ Phase A 可先 `console`/本地;不挡验收。
| Phase | 内容 |
|---|---|
| **A(本 ECR** | 登录 · 用户 · 订单 · 授予会员 · 审计 · admin-h5 壳 |
| **BECR-007** | 自有埋点 · 管理端「数据」看板 — Spec `ops-analytics.md` |
| **CECR-008** | 首页宫格 CRUD · 测评上下架 — Spec `ops-content.md`Approved |
| **BECR-007 Closed** | 自有埋点 · 管理端「数据」看板 — Spec `ops-analytics.md` |
| **CECR-008 Closed** | 首页宫格 CRUD · 测评上下架 — Spec `ops-content.md` |
| D | 订单筛选 · 展示价 · 退款只读(另开 ECR) |
| E | RBAC · 封禁 · 推送占位(另开 ECR) |
+1 -1
View File
@@ -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 -1
View File
@@ -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)
---
+3 -1
View File
@@ -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 EphemerisCGO,默认 Moshier);热带 + Whole Sign |
| 实现 | `POST /reports/star` · `POST /reports/synastry` · nearby/invites · H5 `/star` · `/synastry` · invite |
| Gaps | ECR-012 收口:报告页复用 StarFortunePanel;分享 type=starfortuneBundle 仅读 outlook 且剥离 luckye2e 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{
+24 -1
View File
@@ -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()
+10 -1
View File
@@ -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>
+10 -4
View File
@@ -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) {
+24 -1
View File
@@ -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,
+17
View File
@@ -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()
})
})
+22 -7
View File
@@ -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 || '')
+29 -10
View File
@@ -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>
+19 -9
View File
@@ -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-07Human |
| Author | Architect |
| Date | 2026-08-07 |
| Risk | Low |
---
## Context
- 问题 / 目标:星座/合盘主路径已落地,需 Spec 对齐、H5/报告页一致、测试加厚;默认 **不改表结构**
- 非目标:真支付、新模块、换星历、Ops。
- 相关 Spec`star-profile.md`;前序 ECR-003fortune→outlook)。
## Architecture Change
- 是否改变分层边界:**No**
- 受影响层:Application`internal/star` 仅按需)· API 文档 · UIuser-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
- RequiredNO
- 回滚:代码回滚即可
## Failure Handling
- 无档案 / 缺生日:Empty 表单(现有)
- detail 无权益:服务端剥离(现有,须保持测试)
- 旧报告缺 outlookH5 空态,不伪造 fortune
## Test Plan
- Unitoutlook bundle keysH5 fortuneBundleFrom
- Integration`TestFlowStarDeepAccess` · synastry charts keys · 禁用词
- E2Ep2 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` 回归
+8
View File
@@ -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 G1G6**真支付排除**
- **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
+10 -1
View File
@@ -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 decision2026-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
```
+9
View File
@@ -32,3 +32,12 @@ Date: 2026-08-07 · Reviewer pass → P1 fixed
- [x] Admin / Device 鉴权隔离
- [x] 未引入非默认中间件
- [x] 审计与写操作同事务
## Closed decision2026-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
```
+4 -2
View File
@@ -1,9 +1,10 @@
# ECR-007
**Title:** 运营行为分析 Ops-B(自有埋点采集 · 管理端数据看板)
**Status:** Approved
**Status:** Closed
**Date:** 2026-08-07
**Approved:** 2026-08-07Human
**Closed:** 2026-08-07Reviewer · Approve + ess-validate review PASS
**Change Level:** L2(新能力;栈与分层不变)
## Change
@@ -50,8 +51,9 @@
- [x] IntegrationH5 形态上报 → 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 ClosedSTATE · TRACEABILITY
## Rollback
+4 -2
View File
@@ -1,9 +1,10 @@
# ECR-008
**Title:** 运营内容配置 Ops-C(首页宫格 CRUD · 测评上下架)
**Status:** Approved
**Status:** Closed
**Date:** 2026-08-07
**Approved:** 2026-08-07Human
**Closed:** 2026-08-07Reviewer · Approve + ess-validate review PASS
**Change Level:** L2(新能力;栈与分层不变)
## Change
@@ -48,8 +49,9 @@
- [x] IntegrationPUT tools → GET home/toolsPATCH 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 ClosedSTATE · TRACEABILITY
## Rollback
+80
View File
@@ -0,0 +1,80 @@
# ECR-012
**Title:** 星座对齐收口(星盘 · 运势 outlook · 合盘)
**Status:** Approved
**Date:** 2026-08-07
**Approved:** 2026-08-07Human
**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
### ForbiddenNon-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 单测 / e2ep2 star 路径)mock 含 outlook 最小字段
- [x] OpenAPI / TRACEABILITY / STATE / TEST_REPORTCODE_REVIEW 待 Reviewer
- [ ] `python3 scripts/ess-validate.py --phase review --ecr ECR-012`Reviewer
- [x] Docs + CHANGELOGP2-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 spece2e 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):二者均为 ApprovedHuman)→ 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 G1G6
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 收口星座对齐(G1G6)。
## 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 标 ClosedP2-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。
+2 -2
View File
@@ -46,9 +46,9 @@
## Active anchors
- ECR: **ECR-006** 运营后台 Phase AClosedECR-001005 Closed / shipped
- ECR: **ECR-012** 星座对齐(**Implemented** · review);ECR-006008 Ops A/B/C ClosedECR-001005 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**
+7 -5
View File
@@ -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)。
+9 -6
View File
@@ -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)。
+17
View File
@@ -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。
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+18
View File
@@ -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 回归
- 真支付不在范围
+40
View File
@@ -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 剥离 luckyfortuneBundle 仅读 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
+3 -2
View File
@@ -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 Aadmin 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
View File
@@ -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=starsummary 含 chart/planets/aspects_preview/outlook(日周月年一生)/transits;无 fortune/lucky 键;detail 按权益裁剪
content:
application/json:
schema: