Compare commits
5 Commits
e049be280e
...
v1.0.12
| Author | SHA1 | Date | |
|---|---|---|---|
| bed9dce943 | |||
| 4ac42f6575 | |||
| f72312ea52 | |||
| 7cf4636b8c | |||
| 27e4d06da7 |
@@ -189,7 +189,7 @@ cd zhiyin-app && npm run build:mp-weixin && node scripts/upload-mp.js
|
|||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| Phase 0: 战略升级 | ✅ 完成 | 定价重构(免费 + ¥19.9/月),三层壁垒设计 |
|
| Phase 0: 战略升级 | ✅ 完成 | 定价重构(免费 + ¥19.9/月),三层壁垒设计 |
|
||||||
| Phase 0.5: 壁垒构建 | ✅ 完成 | 数据飞轮(面经贡献+题库),留存入围(进步轨迹+打卡日历+每日一题) |
|
| Phase 0.5: 壁垒构建 | ✅ 完成 | 数据飞轮(面经贡献+题库),留存入围(进步轨迹+打卡日历+每日一题) |
|
||||||
| Phase 1: MVP 上线 | 🚧 当前 | 小程序审核提交、微信登录联调、生产部署、100 人内测 |
|
| Phase 1: MVP 上线 | 🚧 当前 | 小程序 v1.0.11 已上传、H5 已部署、生产模式已启用、SMTP 邮箱验证码已配置 |
|
||||||
| Phase 1.5: 商业化 | 📋 规划 | 冲刺版 ¥49.9/月、每日一题定时推送、PMF 验证 |
|
| Phase 1.5: 商业化 | 📋 规划 | 冲刺版 ¥49.9/月、每日一题定时推送、PMF 验证 |
|
||||||
| Phase 2: 增强 + 题库 | 📋 规划 | 50+ 校招岗位、技能缺口分析、公司真题库建设 |
|
| Phase 2: 增强 + 题库 | 📋 规划 | 50+ 校招岗位、技能缺口分析、公司真题库建设 |
|
||||||
| Phase 3: 秋招冲刺 | 📋 规划 | 高校合作、B 端服务、KOC 推广 |
|
| Phase 3: 秋招冲刺 | 📋 规划 | 高校合作、B 端服务、KOC 推广 |
|
||||||
@@ -204,6 +204,7 @@ cd zhiyin-app && npm run build:mp-weixin && node scripts/upload-mp.js
|
|||||||
```
|
```
|
||||||
MONGODB_URI=mongodb://localhost:27017/zhiyin
|
MONGODB_URI=mongodb://localhost:27017/zhiyin
|
||||||
JWT_SECRET=your-strong-secret-at-least-32-chars
|
JWT_SECRET=your-strong-secret-at-least-32-chars
|
||||||
|
NODE_ENV=production
|
||||||
PORT=3006
|
PORT=3006
|
||||||
AI_PRIMARY_KEY=xxx
|
AI_PRIMARY_KEY=xxx
|
||||||
AI_BACKUP_KEY=xxx
|
AI_BACKUP_KEY=xxx
|
||||||
@@ -214,6 +215,12 @@ WECHAT_SERIAL_NO=xxx
|
|||||||
WECHAT_PRIVATE_KEY_PATH=/path/to/apiclient_key.pem
|
WECHAT_PRIVATE_KEY_PATH=/path/to/apiclient_key.pem
|
||||||
WX_DAILY_QUESTION_TMPL=微信订阅消息模板 ID
|
WX_DAILY_QUESTION_TMPL=微信订阅消息模板 ID
|
||||||
CORS_ORIGINS=http://localhost:8888,https://zhiyin.yzrcloud.cn
|
CORS_ORIGINS=http://localhost:8888,https://zhiyin.yzrcloud.cn
|
||||||
|
EMAIL_HOST=smtp.qiye.aliyun.com
|
||||||
|
EMAIL_PORT=465
|
||||||
|
EMAIL_SECURE=true
|
||||||
|
EMAIL_USER=contact@yuzhiran.com
|
||||||
|
EMAIL_PASSWORD=xxx
|
||||||
|
EMAIL_FROM=宇之然AI磁场 <contact@yuzhiran.com>
|
||||||
WHISPER_CPP_PATH=/home/wlt/whisper.cpp # whisper.cpp 路径
|
WHISPER_CPP_PATH=/home/wlt/whisper.cpp # whisper.cpp 路径
|
||||||
WHISPER_MODEL=base # ASR 模型:tiny / base / small
|
WHISPER_MODEL=base # ASR 模型:tiny / base / small
|
||||||
WHISPER_LANGUAGE=zh # ASR 语言
|
WHISPER_LANGUAGE=zh # ASR 语言
|
||||||
@@ -228,7 +235,19 @@ VITE_APP_NAME=AI磁场
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 八、Git
|
## 八、测试 / 管理员账号
|
||||||
|
|
||||||
|
| 账号 | 密码 | 角色 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `13701190814@139.com` | `Zhiyin2024!` | admin | 管理员,可访问管理后台 |
|
||||||
|
| `test@yzrcloud.cn` | `123456` | user | 测试账号 |
|
||||||
|
| `test@test.com` | 验证码 `123456` | admin | 旧管理员(dev 模式可用) |
|
||||||
|
|
||||||
|
管理后台路径:`/pages/admin/admin`,进入后自动验证管理员身份(`onMounted` → `doVerify`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、Git
|
||||||
|
|
||||||
- 远程仓库: `http://127.0.0.1:2999/txai-dev/zhiyin.git`(本机 Gitea,带 token 认证)
|
- 远程仓库: `http://127.0.0.1:2999/txai-dev/zhiyin.git`(本机 Gitea,带 token 认证)
|
||||||
- 默认分支: `master`
|
- 默认分支: `master`
|
||||||
@@ -236,7 +255,7 @@ VITE_APP_NAME=AI磁场
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 九、技术细节与坑
|
## 十、技术细节与坑
|
||||||
|
|
||||||
1. **DOMMatrix polyfill**: `main.ts` 顶部有 pdf-parse 所需的浏览器 API polyfill(DOMMatrix / DOMPoint),新增 PDF 相关功能时注意兼容性
|
1. **DOMMatrix polyfill**: `main.ts` 顶部有 pdf-parse 所需的浏览器 API polyfill(DOMMatrix / DOMPoint),新增 PDF 相关功能时注意兼容性
|
||||||
2. **postbuild**: `backend/package.json` 中的 `postbuild` 脚本自动复制 `certs/` 到 `dist/src/certs/`,这是微信支付证书的必要步骤
|
2. **postbuild**: `backend/package.json` 中的 `postbuild` 脚本自动复制 `certs/` 到 `dist/src/certs/`,这是微信支付证书的必要步骤
|
||||||
@@ -244,14 +263,15 @@ VITE_APP_NAME=AI磁场
|
|||||||
4. **前端 API 调用**: `zhiyin-app/src/services/api.ts` 封装了 `uni.request`,自动处理 token 注入(从 `uni.getStorageSync('token')`)和 401 过期跳转
|
4. **前端 API 调用**: `zhiyin-app/src/services/api.ts` 封装了 `uni.request`,自动处理 token 注入(从 `uni.getStorageSync('token')`)和 401 过期跳转
|
||||||
5. **前端环境判断**: `config.ts` 中使用 `// #ifdef H5` / `// #ifdef MP-WEIXIN` 条件编译区分 H5 和小程序
|
5. **前端环境判断**: `config.ts` 中使用 `// #ifdef H5` / `// #ifdef MP-WEIXIN` 条件编译区分 H5 和小程序
|
||||||
6. **API 限流**: 100 次/60 秒(在 `app.module.ts` 中配置),注意避免在定时任务和批量操作中被限
|
6. **API 限流**: 100 次/60 秒(在 `app.module.ts` 中配置),注意避免在定时任务和批量操作中被限
|
||||||
7. **验证码**: 开发模式下手机验证码固定为 `123456`(`user.service.ts` 中实现),生产环境需移除
|
7. **验证码**: 生产模式(`NODE_ENV=production`)使用真实 SMTP 发邮件验证码;非生产模式手机验证码固定为 `123456`、邮件验证码在响应中返回 `devCode`
|
||||||
8. **MongoDB**: 8 个核心集合 + 2 个分享集合
|
8. **MongoDB**: 8 个核心集合 + 2 个分享集合
|
||||||
9. **api.ts 陷阱**: 对象字面量必须在 `export const apiService = {` 或 `const apiService = { ... export default apiService` 中包裹,否则 uni-app 构建报错 `Expected ";" but found ":"`。git pull 后经常丢失这行声明,需手动补回
|
9. **api.ts 陷阱**: 对象字面量必须在 `export const apiService = {` 或 `const apiService = { ... export default apiService` 中包裹,否则 uni-app 构建报错 `Expected ";" but found ":"`。git pull 后经常丢失这行声明,需手动补回
|
||||||
10. **H5 构建 assets 清理**: `assets/` 中的旧 hash 文件不能随意删除——`index-*.js`(主 bundle)动态 import 了所有 page chunk,删除仍在引用的文件会导致浏览器 `NS_ERROR_CORRUPTED_CONTENT`
|
10. **H5 构建 assets 清理**: `assets/` 中的旧 hash 文件不能随意删除——`index-*.js`(主 bundle)动态 import 了所有 page chunk,删除仍在引用的文件会导致浏览器 `NS_ERROR_CORRUPTED_CONTENT`
|
||||||
|
11. **管理后台自动验证**: `admin.vue` 中 `onMounted` 自动调用 `doVerify()`,进入后台即检测 JWT 中 `role` 是否为 `admin`,不再需要手动点击"验证管理员身份"按钮
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 十、交付检查清单(每次实施/修改后执行)
|
## 十一、交付检查清单(每次实施/修改后执行)
|
||||||
|
|
||||||
### Step 1: 代码评审
|
### Step 1: 代码评审
|
||||||
- [ ] 是否符合现有模块模式(schema→service→controller→module)
|
- [ ] 是否符合现有模块模式(schema→service→controller→module)
|
||||||
|
|||||||
+28
-1
@@ -1,7 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
onLaunch(() => { console.log('职引 App launched') })
|
onLaunch(() => {
|
||||||
|
console.log('职引 App launched')
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
initPrivacy()
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
function initPrivacy() {
|
||||||
|
if (wx.onNeedPrivacyAuthorization) {
|
||||||
|
wx.onNeedPrivacyAuthorization((resolve) => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '隐私政策授权',
|
||||||
|
content: '请仔细阅读并同意《用户服务协议》和《隐私政策》后再使用本应用。您的个人信息将仅用于求职服务。',
|
||||||
|
confirmText: '同意',
|
||||||
|
cancelText: '拒绝',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
resolve({ event: 'agree' })
|
||||||
|
} else {
|
||||||
|
resolve({ event: 'disagree' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
onShow(() => { console.log('职引 App shown') })
|
onShow(() => { console.log('职引 App shown') })
|
||||||
onHide(() => { console.log('职引 App hidden') })
|
onHide(() => { console.log('职引 App hidden') })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "宇之然AI磁场",
|
"name": "宇之然AI磁场",
|
||||||
"appid": "__UNI__DEV__",
|
"appid": "__UNI__DEV__",
|
||||||
"versionName": "1.0.11",
|
"versionName": "1.0.12",
|
||||||
"versionCode": "111",
|
"versionCode": "112",
|
||||||
"description": "职引 - 宇之然AI磁场旗下AI模拟面试平台,提供AI面试官模拟练习、简历智能优化、大厂面经题库,助你轻松应对校招面试。",
|
"description": "职引 - 宇之然AI磁场旗下AI模拟面试平台,提供AI面试官模拟练习、简历智能优化、大厂面经题库,助你轻松应对校招面试。",
|
||||||
"h5": {
|
"h5": {
|
||||||
"title": "职引 - AI模拟面试 | 宇之然AI磁场",
|
"title": "职引 - AI模拟面试 | 宇之然AI磁场",
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"mp-weixin": {
|
"mp-weixin": {
|
||||||
"appid": "wxf466b3c3bc411ffc",
|
"appid": "wxf466b3c3bc411ffc",
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": false
|
"urlCheck": false,
|
||||||
|
"__usePrivacyCheck__": true
|
||||||
},
|
},
|
||||||
"usingComponents": true
|
"usingComponents": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,7 +368,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { api, API_ENDPOINTS } from '../../config'
|
import { api, API_ENDPOINTS } from '../../config'
|
||||||
|
|
||||||
const verified = ref(false)
|
const verified = ref(false)
|
||||||
@@ -777,6 +777,8 @@ const doAdjustCredits = async () => {
|
|||||||
loadUsers()
|
loadUsers()
|
||||||
} catch { uni.showToast({ title: '调整失败', icon: 'none' }) }
|
} catch { uni.showToast({ title: '调整失败', icon: 'none' }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => { doVerify() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<text class="field-label">密码</text>
|
<text class="field-label">密码</text>
|
||||||
<input class="input" type="password" v-model="password" placeholder="请输入密码" @confirm="doPasswordLogin" />
|
<input class="input" type="password" v-model="password" placeholder="请输入密码" @confirm="doPasswordLogin" />
|
||||||
</view>
|
</view>
|
||||||
<button class="login-btn" :disabled="!canPasswordLogin || pwdLoading" @click="doPasswordLogin">
|
<button class="login-btn" :disabled="!canPasswordLogin || pwdLoading || !agreed" @click="doPasswordLogin">
|
||||||
{{ pwdLoading ? '登录中...' : '登录' }}
|
{{ pwdLoading ? '登录中...' : '登录' }}
|
||||||
</button>
|
</button>
|
||||||
<view class="switch-hint" @click="loginMode='code'">忘记密码?使用验证码登录</view>
|
<view class="switch-hint" @click="loginMode='code'">忘记密码?使用验证码登录</view>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<text class="field-label">验证码</text>
|
<text class="field-label">验证码</text>
|
||||||
<input class="input" type="number" maxlength="6" v-model="emailCode" placeholder="请输入6位验证码" />
|
<input class="input" type="number" maxlength="6" v-model="emailCode" placeholder="请输入6位验证码" />
|
||||||
</view>
|
</view>
|
||||||
<button class="login-btn" :disabled="!emailSent || !emailCode || emailLoading" @click="doEmailLogin">
|
<button class="login-btn" :disabled="!emailSent || !emailCode || emailLoading || !agreed" @click="doEmailLogin">
|
||||||
{{ emailLoading ? '登录中...' : '登录' }}
|
{{ emailLoading ? '登录中...' : '登录' }}
|
||||||
</button>
|
</button>
|
||||||
<view class="switch-hint" @click="loginMode='password'">已有密码?使用密码登录</view>
|
<view class="switch-hint" @click="loginMode='password'">已有密码?使用密码登录</view>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<text class="field-label">确认密码</text>
|
<text class="field-label">确认密码</text>
|
||||||
<input class="input" type="password" v-model="confirmPassword" placeholder="再次输入密码" @confirm="doRegister" />
|
<input class="input" type="password" v-model="confirmPassword" placeholder="再次输入密码" @confirm="doRegister" />
|
||||||
</view>
|
</view>
|
||||||
<button class="login-btn" :disabled="!canRegister || regLoading" @click="doRegister">
|
<button class="login-btn" :disabled="!canRegister || regLoading || !agreed" @click="doRegister">
|
||||||
{{ regLoading ? '注册中...' : '注册' }}
|
{{ regLoading ? '注册中...' : '注册' }}
|
||||||
</button>
|
</button>
|
||||||
<view class="switch-hint" @click="mainTab='login'">已有账号?去登录</view>
|
<view class="switch-hint" @click="mainTab='login'">已有账号?去登录</view>
|
||||||
@@ -87,15 +87,18 @@
|
|||||||
<view class="card" v-if="mainTab === 'wechat' && isMp">
|
<view class="card" v-if="mainTab === 'wechat' && isMp">
|
||||||
<text class="card-title">微信一键登录</text>
|
<text class="card-title">微信一键登录</text>
|
||||||
<text class="card-sub">授权后自动创建账号</text>
|
<text class="card-sub">授权后自动创建账号</text>
|
||||||
<button class="login-btn wx-btn" @click="doWxLogin">{{ wxLoading ? '登录中...' : '微信一键登录' }}</button>
|
<button class="login-btn wx-btn" :disabled="wxLoading || !agreed" @click="doWxLogin">{{ wxLoading ? '登录中...' : '微信一键登录' }}</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 法律声明 -->
|
<!-- 法律声明 - 用户自主勾选同意 -->
|
||||||
<view class="legal">
|
<view class="legal">
|
||||||
<text class="legal-text">登录即表示同意</text>
|
<label class="agree-label">
|
||||||
<text class="legal-link" @click="goAgreement">《用户协议》</text>
|
<checkbox :checked="agreed" @tap="agreed = !agreed" color="#4F46E5" style="transform:scale(0.85)" />
|
||||||
<text class="legal-text">和</text>
|
<text class="agree-text">我已阅读并同意</text>
|
||||||
<text class="legal-link" @click="goPrivacy">《隐私政策》</text>
|
<text class="legal-link" @tap.stop="goAgreement">《用户服务协议》</text>
|
||||||
|
<text class="agree-text">和</text>
|
||||||
|
<text class="legal-link" @tap.stop="goPrivacy">《隐私政策》</text>
|
||||||
|
</label>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -117,6 +120,8 @@
|
|||||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { api } from '../../config'
|
import { api } from '../../config'
|
||||||
|
|
||||||
|
const agreed = ref(false)
|
||||||
|
|
||||||
const mainTab = ref('login')
|
const mainTab = ref('login')
|
||||||
const loginMode = ref('password') // 'password' | 'code'
|
const loginMode = ref('password') // 'password' | 'code'
|
||||||
const isMp = ref(false)
|
const isMp = ref(false)
|
||||||
@@ -192,6 +197,9 @@ const sendEmailCode = () => {
|
|||||||
emailSent.value = true
|
emailSent.value = true
|
||||||
showToast('验证码已发送', 'success')
|
showToast('验证码已发送', 'success')
|
||||||
startCooldown()
|
startCooldown()
|
||||||
|
if (res.data?.devCode) {
|
||||||
|
uni.showModal({ title: '开发模式', content: `验证码:${res.data.devCode}`, showCancel: false })
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const msg = (res.data && res.data.message) || '发送失败'
|
const msg = (res.data && res.data.message) || '发送失败'
|
||||||
showToast(msg)
|
showToast(msg)
|
||||||
@@ -354,8 +362,9 @@ const goPrivacy = () => uni.navigateTo({ url: '/pages/privacy/privacy' })
|
|||||||
.switch-hint { text-align: center; font-size: 22rpx; color: var(--color-primary); padding: 20rpx 0 4rpx; }
|
.switch-hint { text-align: center; font-size: 22rpx; color: var(--color-primary); padding: 20rpx 0 4rpx; }
|
||||||
|
|
||||||
/* ===== Legal ===== */
|
/* ===== Legal ===== */
|
||||||
.legal { display: flex; justify-content: center; align-items: center; gap: 4rpx; margin-top: 24rpx; flex-wrap: wrap; }
|
.legal { display: flex; justify-content: center; align-items: center; margin-top: 24rpx; flex-wrap: wrap; }
|
||||||
.legal-text { font-size: 22rpx; color: var(--color-text-tertiary); }
|
.agree-label { display: flex; align-items: center; gap: 6rpx; flex-wrap: wrap; justify-content: center; }
|
||||||
|
.agree-text { font-size: 22rpx; color: var(--color-text-tertiary); }
|
||||||
.legal-link { font-size: 22rpx; color: var(--color-primary); }
|
.legal-link { font-size: 22rpx; color: var(--color-primary); }
|
||||||
|
|
||||||
/* ===== Password Modal ===== */
|
/* ===== Password Modal ===== */
|
||||||
|
|||||||
Reference in New Issue
Block a user