6 Commits

Author SHA1 Message Date
e605da786a fix: CI用timeout命令强制3分钟超时+清理Cypress进程
All checks were successful
Deploy Production / test (push) Successful in 6s
Deploy Production / deploy (push) Successful in 5s
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
Test / e2e-test (push) Successful in 49s
PR Preview / teardown-preview (pull_request) Has been skipped
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 10s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:23:57 +00:00
87e24773aa fix: CI增加requestTimeout和responseTimeout
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 8s
Test / e2e-test (push) Failing after 10m40s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:12:30 +00:00
026ff18e92 test: 新增英文搜索单元测试(5个)
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 14s
Test / e2e-test (push) Failing after 29m2s
- oilEn翻译验证(已知/未知精油)
- 英文查询检测
- 英文匹配精油名和配方en_name

全部通过: 209 unit + 36 e2e

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:41:34 +00:00
6448c24caf feat: 商业认证审核显示上传的证明图片
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 15s
Test / e2e-test (push) Successful in 50s
- 审核列表显示缩略图(60x60),点击查看大图
- 全屏遮罩预览,点击关闭

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:34:52 +00:00
5cd954ccad feat: 英文搜索+全部配方翻译
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 13s
Test / e2e-test (push) Successful in 52s
- 搜索框支持英文:自动匹配配方英文名和精油英文名
- 292条配方全部翻译英文名(本地+线上同步)
- 输入英文时搜索范围包含en_name和精油英文名

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:17:27 +00:00
c53dda0622 fix: CI动态端口+超时控制,避免e2e卡死
All checks were successful
PR Preview / teardown-preview (pull_request) Successful in 15s
PR Preview / test (pull_request) Has been skipped
PR Preview / deploy-preview (pull_request) Has been skipped
Deploy Production / test (push) Successful in 8s
Test / unit-test (push) Successful in 6s
Deploy Production / deploy (push) Successful in 8s
Test / build-check (push) Successful in 4s
Test / e2e-test (push) Successful in 51s
- 后端端口随机9000-9999,前端4000-4999
- 数据库文件按端口号隔离
- vite proxy支持VITE_API_PORT环境变量
- 服务启动超时30秒,失败即退出
- Cypress: defaultCommandTimeout=5s, pageLoadTimeout=10s
- 整个e2e job timeout 5分钟

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:11:02 +00:00
5 changed files with 102 additions and 17 deletions

View File

@@ -12,6 +12,7 @@ jobs:
e2e-test: e2e-test:
runs-on: test runs-on: test
needs: unit-test needs: unit-test
timeout-minutes: 5
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -23,26 +24,43 @@ jobs:
- name: E2E tests - name: E2E tests
run: | run: |
# Dynamic ports to avoid conflicts
BE_PORT=$(shuf -i 9000-9999 -n 1)
FE_PORT=$(shuf -i 4000-4999 -n 1)
DB_FILE="/tmp/ci_oil_test_${BE_PORT}.db"
echo "Using backend=$BE_PORT frontend=$FE_PORT db=$DB_FILE"
# Start backend # Start backend
DB_PATH=/tmp/ci_oil_test.db FRONTEND_DIR=/dev/null \ DB_PATH="$DB_FILE" FRONTEND_DIR=/dev/null \
/tmp/ci-venv/bin/uvicorn backend.main:app --port 8000 & /tmp/ci-venv/bin/uvicorn backend.main:app --port $BE_PORT &
BE_PID=$!
# Start frontend (in subshell to not change cwd) # Start frontend with proxy to dynamic backend port
(cd frontend && npx vite --port 5173) & (cd frontend && VITE_API_PORT=$BE_PORT npx vite --port $FE_PORT) &
FE_PID=$!
# Wait for both servers # Wait for both servers (max 30s, fail fast)
READY=0
for i in $(seq 1 30); do for i in $(seq 1 30); do
if curl -sf http://localhost:8000/api/version > /dev/null 2>&1 && \ if curl -sf http://localhost:$BE_PORT/api/oils > /dev/null 2>&1 && \
curl -sf http://localhost:5173/ > /dev/null 2>&1; then curl -sf http://localhost:$FE_PORT/ > /dev/null 2>&1; then
echo "Both servers ready" echo "Both servers ready in ${i}s"
READY=1
break break
fi fi
sleep 1 sleep 1
done done
# Run core cypress specs (proven stable) if [ "$READY" = "0" ]; then
echo "ERROR: Servers failed to start within 30s"
kill $BE_PID $FE_PID 2>/dev/null
rm -f "$DB_FILE"
exit 1
fi
# Run core cypress specs with hard 3-minute timeout
cd frontend cd frontend
npx cypress run --spec "\ timeout 180 npx cypress run --spec "\
cypress/e2e/recipe-detail.cy.js,\ cypress/e2e/recipe-detail.cy.js,\
cypress/e2e/oil-reference.cy.js,\ cypress/e2e/oil-reference.cy.js,\
cypress/e2e/oil-data-integrity.cy.js,\ cypress/e2e/oil-data-integrity.cy.js,\
@@ -50,13 +68,17 @@ jobs:
cypress/e2e/category-modules.cy.js,\ cypress/e2e/category-modules.cy.js,\
cypress/e2e/notification-flow.cy.js,\ cypress/e2e/notification-flow.cy.js,\
cypress/e2e/registration-flow.cy.js\ cypress/e2e/registration-flow.cy.js\
" --config video=false " --config "video=false,defaultCommandTimeout=5000,pageLoadTimeout=10000,requestTimeout=5000,responseTimeout=10000,baseUrl=http://localhost:$FE_PORT"
EXIT_CODE=$? EXIT_CODE=$?
# Cleanup # Cleanup
pkill -f "uvicorn backend" || true kill $BE_PID $FE_PID 2>/dev/null
pkill -f "node.*vite" || true pkill -f "Cypress" 2>/dev/null || true
rm -f /tmp/ci_oil_test.db rm -f "$DB_FILE"
if [ $EXIT_CODE -eq 124 ]; then
echo "ERROR: Cypress timed out after 3 minutes"
exit 1
fi
exit $EXIT_CODE exit $EXIT_CODE
build-check: build-check:

View File

@@ -85,3 +85,51 @@ describe('EDITOR_ONLY_TAGS', () => {
expect(EDITOR_ONLY_TAGS).toContain('已审核') expect(EDITOR_ONLY_TAGS).toContain('已审核')
}) })
}) })
// ---------------------------------------------------------------------------
// English search
// ---------------------------------------------------------------------------
describe('English search matching', () => {
const { oilEn } = require('../composables/useOilTranslation')
it('oilEn returns English name for known oils', () => {
expect(oilEn('薰衣草')).toBe('Lavender')
expect(oilEn('茶树')).toBe('Tea Tree')
expect(oilEn('乳香')).toBe('Frankincense')
})
it('oilEn returns empty for unknown oils', () => {
expect(oilEn('不存在的油')).toBeFalsy()
})
it('English query detection', () => {
const isEn = (q) => /^[a-zA-Z\s]+$/.test(q)
expect(isEn('lavender')).toBe(true)
expect(isEn('Tea Tree')).toBe(true)
expect(isEn('薰衣草')).toBe(false)
expect(isEn('lav3')).toBe(false)
})
it('English matches oil name in recipe', () => {
const recipe = {
name: '助眠配方',
en_name: 'Sleep Aid Blend',
ingredients: [{ oil: '薰衣草', drops: 3 }],
tags: []
}
const q = 'lavender'
const isEn = /^[a-zA-Z\s]+$/.test(q)
const enNameMatch = isEn && (recipe.en_name || '').toLowerCase().includes(q)
const oilEnMatch = isEn && recipe.ingredients.some(ing => (oilEn(ing.oil) || '').toLowerCase().includes(q))
expect(oilEnMatch).toBe(true)
expect(enNameMatch).toBe(false)
})
it('English matches recipe en_name', () => {
const recipe = { name: '助眠', en_name: 'Sleep Aid Blend', ingredients: [], tags: [] }
const q = 'sleep'
const isEn = /^[a-zA-Z\s]+$/.test(q)
const enNameMatch = isEn && (recipe.en_name || '').toLowerCase().includes(q)
expect(enNameMatch).toBe(true)
})
})

View File

@@ -175,6 +175,7 @@ import { useUiStore } from '../stores/ui'
import { api } from '../composables/useApi' import { api } from '../composables/useApi'
import RecipeCard from '../components/RecipeCard.vue' import RecipeCard from '../components/RecipeCard.vue'
import RecipeDetailOverlay from '../components/RecipeDetailOverlay.vue' import RecipeDetailOverlay from '../components/RecipeDetailOverlay.vue'
import { oilEn } from '../composables/useOilTranslation'
const auth = useAuthStore() const auth = useAuthStore()
const oils = useOilsStore() const oils = useOilsStore()
@@ -313,11 +314,14 @@ function expandQuery(q) {
const exactResults = computed(() => { const exactResults = computed(() => {
if (!searchQuery.value.trim()) return [] if (!searchQuery.value.trim()) return []
const q = searchQuery.value.trim().toLowerCase() const q = searchQuery.value.trim().toLowerCase()
const isEn = /^[a-zA-Z\s]+$/.test(q)
return recipeStore.recipes.filter(r => { return recipeStore.recipes.filter(r => {
const nameMatch = r.name.toLowerCase().includes(q) const nameMatch = r.name.toLowerCase().includes(q)
const enNameMatch = isEn && (r.en_name || '').toLowerCase().includes(q)
const oilEnMatch = isEn && r.ingredients.some(ing => (oilEn(ing.oil) || '').toLowerCase().includes(q))
const visibleTags = auth.canEdit ? (r.tags || []) : (r.tags || []).filter(t => !EDITOR_ONLY_TAGS.includes(t)) const visibleTags = auth.canEdit ? (r.tags || []) : (r.tags || []).filter(t => !EDITOR_ONLY_TAGS.includes(t))
const tagMatch = visibleTags.some(t => t.toLowerCase().includes(q)) const tagMatch = visibleTags.some(t => t.toLowerCase().includes(q))
return nameMatch || tagMatch return nameMatch || enNameMatch || oilEnMatch || tagMatch
}).sort((a, b) => a.name.localeCompare(b.name, 'zh')) }).sort((a, b) => a.name.localeCompare(b.name, 'zh'))
}) })

View File

@@ -31,6 +31,7 @@
<span class="review-name">{{ group.latest.display_name || group.latest.username }}</span> <span class="review-name">{{ group.latest.display_name || group.latest.username }}</span>
<span class="review-reason">商户名{{ group.latest.business_name }}</span> <span class="review-reason">商户名{{ group.latest.business_name }}</span>
<span class="biz-status-tag" :class="'biz-' + group.effectiveStatus">{{ { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[group.effectiveStatus] }}</span> <span class="biz-status-tag" :class="'biz-' + group.effectiveStatus">{{ { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[group.effectiveStatus] }}</span>
<img v-if="group.latest.document && group.latest.document.startsWith('data:image')" :src="group.latest.document" class="biz-doc-preview" @click="showDocFull = group.latest.document" />
</div> </div>
<div class="review-actions"> <div class="review-actions">
<template v-if="group.effectiveStatus === 'pending'"> <template v-if="group.effectiveStatus === 'pending'">
@@ -111,6 +112,11 @@
</div> </div>
<div class="user-count"> {{ users.length }} 个用户</div> <div class="user-count"> {{ users.length }} 个用户</div>
<!-- Full-size document preview -->
<div v-if="showDocFull" class="doc-overlay" @click="showDocFull = null">
<img :src="showDocFull" class="doc-full-img" />
</div>
</div> </div>
</template> </template>
@@ -128,6 +134,7 @@ const users = ref([])
const searchQuery = ref('') const searchQuery = ref('')
const filterRole = ref('') const filterRole = ref('')
const translations = ref([]) const translations = ref([])
const showDocFull = ref(null)
const businessApps = ref([]) const businessApps = ref([])
import { reactive } from 'vue' import { reactive } from 'vue'
@@ -443,6 +450,10 @@ onMounted(() => {
} }
.biz-reject-reason { color: #c62828; font-size: 11px; } .biz-reject-reason { color: #c62828; font-size: 11px; }
.biz-time { color: #bbb; font-size: 11px; margin-left: auto; } .biz-time { color: #bbb; font-size: 11px; margin-left: auto; }
.biz-doc-preview { width: 60px; height: 60px; object-fit: cover; border-radius: 6px; cursor: pointer; border: 1px solid #e5e4e7; margin-top: 6px; }
.biz-doc-preview:hover { border-color: #7ec6a4; }
.doc-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 1000; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.doc-full-img { max-width: 90vw; max-height: 90vh; border-radius: 10px; }
.btn-approve { .btn-approve {
background: #4a9d7e; background: #4a9d7e;

View File

@@ -10,8 +10,8 @@ export default defineConfig({
plugins: [vue()], plugins: [vue()],
server: { server: {
proxy: { proxy: {
'/api': 'http://localhost:8000', '/api': `http://localhost:${process.env.VITE_API_PORT || 8000}`,
'/uploads': 'http://localhost:8000' '/uploads': `http://localhost:${process.env.VITE_API_PORT || 8000}`
} }
}, },
build: { build: {