xsh_1997 преди 2 седмици
родител
ревизия
52ee991b53

+ 50 - 17
doc/消费者APP/会员注册登录/会员注册登录前端技术方案.md

@@ -1,9 +1,9 @@
1
 # 会员注册登录 — 前端技术方案(C 端 · shop-app)
1
 # 会员注册登录 — 前端技术方案(C 端 · shop-app)
2
 
2
 
3
-> **依据:** 《会员注册登录技术方案.md》v1.2  
3
+> **依据:** 《会员注册登录技术方案.md》v1.3  
4
 > **关联:** 《商品详情内页前端技术方案》(加购登录引导)、《搜索页前端技术方案》  
4
 > **关联:** 《商品详情内页前端技术方案》(加购登录引导)、《搜索页前端技术方案》  
5
 > **范围:** 消费者 APP **会员注册、登录、服务协议勾选**;**不** 改后端、**不** 实现短信验证码、忘记密码、资料/地址编辑(另册)。  
5
 > **范围:** 消费者 APP **会员注册、登录、服务协议勾选**;**不** 改后端、**不** 实现短信验证码、忘记密码、资料/地址编辑(另册)。  
6
-> **实现状态:** 页面与 API 已落地,待与 `/api/member/**` 联调。
6
+> **实现状态:** 页面与 API 已落地;登录 Token 解析已对齐 v1.3 根级响应,待联调。
7
 
7
 
8
 ---
8
 ---
9
 
9
 
@@ -12,9 +12,10 @@
12
 | 项 | 说明 |
12
 | 项 | 说明 |
13
 |----|------|
13
 |----|------|
14
 | 框架 | uni-app **Vue 3** + **uview-plus** |
14
 | 框架 | uni-app **Vue 3** + **uview-plus** |
15
-| 请求 | `@/utils/request`;成功 `code=200`,业务数据在 `data` |
15
+| 请求 | `@/utils/request`;成功 `code=200`;业务体在 `data`,**登录例外**见 §4.2 |
16
 | Token | `config` → `TOKEN_KEY = shop-Admin-Token`;请求头 `Authorization: Bearer {token}` |
16
 | Token | `config` → `TOKEN_KEY = shop-Admin-Token`;请求头 `Authorization: Bearer {token}` |
17
-| 登录账号 | **手机号** 或 **会员名称**(`memberCode`,产品所称「会员 ID」) |
17
+| 鉴权机制 | 与平台一致:`TokenService` 签发 JWT,Redis 会话;不足 20 分钟自动续期(后端) |
18
+| 登录账号 | **手机号** 或 **会员名称**(`memberCode`;产品 **「会员 ID」= 会员名称**,**非** 数字 `memberId`) |
18
 | 注册 | **无短信验证码**;`mobile + password + confirmPassword` |
19
 | 注册 | **无短信验证码**;`mobile + password + confirmPassword` |
19
 | 样式 | 登录/注册共用 `styles/auth.scss` |
20
 | 样式 | 登录/注册共用 `styles/auth.scss` |
20
 
21
 
@@ -44,10 +45,10 @@
44
 |------|------|------|
45
 |------|------|------|
45
 | 登录页 | `shop-app/pages/login/index.vue` | 账号+密码+协议+记住账号 |
46
 | 登录页 | `shop-app/pages/login/index.vue` | 账号+密码+协议+记住账号 |
46
 | 注册页 | `shop-app/subpackage/account/register.vue` | 手机号注册表单 |
47
 | 注册页 | `shop-app/subpackage/account/register.vue` | 手机号注册表单 |
47
-| 协议块 | `shop-app/components/account/AgreementBlock.vue` | 勾选 + 弹窗 **`rich-text`** 渲染(同商品详情) |
48
+| 协议块 | `shop-app/components/account/AgreementBlock.vue` | 勾选 + 弹窗 **`rich-text`** 渲染 |
48
 | HTML 预处理 | `shop-app/utils/htmlContent.js` | 实体解码、图片 URL、空 `<p>` 清理 |
49
 | HTML 预处理 | `shop-app/utils/htmlContent.js` | 实体解码、图片 URL、空 `<p>` 清理 |
49
 | API | `shop-app/api/member.js` | register / login / serviceAgreement / profile |
50
 | API | `shop-app/api/member.js` | register / login / serviceAgreement / profile |
50
-| 状态 | `shop-app/store/user.js` | 登录态、资料缓存 |
51
+| 状态 | `shop-app/store/user.js` | 登录态、**根级 token** 解析、资料缓存 |
51
 | 协议加载 | `shop-app/utils/memberAgreement.js` | 初始化协议配置 |
52
 | 协议加载 | `shop-app/utils/memberAgreement.js` | 初始化协议配置 |
52
 | 校验 | `shop-app/utils/memberValidate.js` | 手机号、密码 |
53
 | 校验 | `shop-app/utils/memberValidate.js` | 手机号、密码 |
53
 | 登录引导 | `shop-app/utils/apiAuth.js` | `ensureApiToken` → 登录页 |
54
 | 登录引导 | `shop-app/utils/apiAuth.js` | `ensureApiToken` → 登录页 |
@@ -69,6 +70,8 @@
69
 | `getMemberServiceAgreement()` | GET | `/api/member/serviceAgreement` | 匿名 |
70
 | `getMemberServiceAgreement()` | GET | `/api/member/serviceAgreement` | 匿名 |
70
 | `getMemberProfile()` | GET | `/api/member/profile` | Token |
71
 | `getMemberProfile()` | GET | `/api/member/profile` | Token |
71
 
72
 
73
+可选(本期未单独封装):`GET /api/member/serviceAgreement/status` — 仅开关;注册/登录页用完整 `serviceAgreement` 即可。
74
+
72
 ### 4.1 注册 Body
75
 ### 4.1 注册 Body
73
 
76
 
74
 | 字段 | 必填 | 前端 |
77
 | 字段 | 必填 | 前端 |
@@ -79,15 +82,40 @@
79
 | memberCode | 否 | 会员名称,空则后端自动生成 |
82
 | memberCode | 否 | 会员名称,空则后端自动生成 |
80
 | agreementAccepted | 条件 | 协议 `enabled` 时须勾选 |
83
 | agreementAccepted | 条件 | 协议 `enabled` 时须勾选 |
81
 
84
 
82
-### 4.2 登录 Body
85
+**成功 `data`:** 系统主键 `memberId`(long);**不能** 作为登录账号,注册成功后跳转登录页用 **手机号** 登录。
86
+
87
+### 4.2 登录 Body 与响应(v1.3)
83
 
88
 
84
 | 字段 | 必填 | 前端 |
89
 | 字段 | 必填 | 前端 |
85
 |------|:----:|------|
90
 |------|:----:|------|
86
-| account | 是 | 手机号或会员名称 |
91
+| account | 是 | 手机号或会员名称(产品所称会员 ID) |
87
 | password | 是 | — |
92
 | password | 是 | — |
88
 | agreementAccepted | 条件 | `requireAgreementOnLogin` 为 true 时须勾选 |
93
 | agreementAccepted | 条件 | `requireAgreementOnLogin` 为 true 时须勾选 |
89
 
94
 
90
-**成功 `data`:** `{ token, memberId }`(`memberId` 为系统主键,**不能**当登录账号)。
95
+**成功响应(与平台 `POST /login` 一致,字段在 JSON 根级):**
96
+
97
+```json
98
+{
99
+  "code": 200,
100
+  "msg": "操作成功",
101
+  "token": "eyJhbGciOiJIUzUxMiJ9...",
102
+  "memberId": 1001
103
+}
104
+```
105
+
106
+| 字段 | 说明 |
107
+|------|------|
108
+| `token` | 存本地 + 请求头 `Authorization: Bearer {token}` |
109
+| `memberId` | **系统主键**,仅资料展示;**不可** 填入登录框 |
110
+
111
+**`store/user.js` 解析:**
112
+
113
+```javascript
114
+const token = res.token || (res.data && res.data.token)
115
+const memberId = res.memberId != null ? res.memberId : (res.data && res.data.memberId)
116
+```
117
+
118
+> **兼容:** 后端若仍传 `mobile` 且 `account` 为空,由服务端等同 `account=mobile`;前端统一传 `account`。
91
 
119
 
92
 ### 4.3 服务协议 `data` 要点
120
 ### 4.3 服务协议 `data` 要点
93
 
121
 
@@ -107,19 +135,20 @@
107
 ```text
135
 ```text
108
 顶栏品牌区
136
 顶栏品牌区
109
 └── 卡片
137
 └── 卡片
110
-    ├── 账号(手机号或会员名称)
138
+    ├── 账号(手机号或会员名称 / 会员 ID
111
     ├── 密码
139
     ├── 密码
112
     ├── 记住账号(localStorage: shop_login_account)
140
     ├── 记住账号(localStorage: shop_login_account)
113
     ├── AgreementBlock(协议启用且 requireAgreementOnLogin 时显示)
141
     ├── AgreementBlock(协议启用且 requireAgreementOnLogin 时显示)
114
-    ├── 登录按钮 → memberLogin → fetchUserInfo → switchTab 首页
142
+    ├── 登录按钮 → memberLogin → 读根级 token → fetchUserInfo → switchTab 首页
115
     └── 立即注册 → subpackage/account/register
143
     └── 立即注册 → subpackage/account/register
116
 ```
144
 ```
117
 
145
 
118
 | 规则 | 实现 |
146
 | 规则 | 实现 |
119
 |------|------|
147
 |------|------|
120
-| RL3 | 单输入框 `account` |
148
+| RL3 | 单输入框 `account`;文案标明「会员 ID = 会员名称」 |
121
 | RL4 | 错误文案由后端统一返回,前端 Toast `msg` |
149
 | RL4 | 错误文案由后端统一返回,前端 Toast `msg` |
122
 | RL6 | `needAgreement` 计算属性控制勾选 |
150
 | RL6 | `needAgreement` 计算属性控制勾选 |
151
+| RL8 | 不把 `memberId` 当登录账号 |
123
 | 已登录进页 | 有 Token 直接 `switchTab` 首页 |
152
 | 已登录进页 | 有 Token 直接 `switchTab` 首页 |
124
 
153
 
125
 ### 5.2 注册 `subpackage/account/register`
154
 ### 5.2 注册 `subpackage/account/register`
@@ -155,6 +184,7 @@
155
 
184
 
156
 ```text
185
 ```text
157
 memberLogin 成功
186
 memberLogin 成功
187
+    → 解析 res.token(根级)
158
     → setToken(token)
188
     → setToken(token)
159
     → getMemberProfile() 写入 store
189
     → getMemberProfile() 写入 store
160
     → 业务请求带 Authorization
190
     → 业务请求带 Authorization
@@ -167,7 +197,7 @@ request 401(需登录接口)
167
     → fedLogOut + reLaunch 登录页
197
     → fedLogOut + reLaunch 登录页
168
 ```
198
 ```
169
 
199
 
170
-**退出:** 仅本地 `fedLogOut`(后端无 C 端 logout 接口)。
200
+**退出:** 仅本地 `fedLogOut`(C 端无专用 logout 接口;与 v1.3 Token 机制不冲突)。
171
 
201
 
172
 ---
202
 ---
173
 
203
 
@@ -177,10 +207,10 @@ request 401(需登录接口)
177
 |------|----------|
207
 |------|----------|
178
 | RL1 | 注册页无验证码 |
208
 | RL1 | 注册页无验证码 |
179
 | RL2 | 确认密码前端校验 |
209
 | RL2 | 确认密码前端校验 |
180
-| RL3 | 登录 `account` 字段 |
210
+| RL3 | 登录 `account` 字段;UI 提示会员 ID = 会员名称 |
181
 | RL4 | 不区分「用户不存在/密码错误」展示 |
211
 | RL4 | 不区分「用户不存在/密码错误」展示 |
182
 | RL6 | `AgreementBlock` + 提交前校验 |
212
 | RL6 | `AgreementBlock` + 提交前校验 |
183
-| RL8 | UI 提示「会员名称」;`memberId` 仅展示在资料区(若有) |
213
+| RL8 | `memberId` 仅 store/资料;登录框不用数字主键 |
184
 
214
 
185
 ---
215
 ---
186
 
216
 
@@ -188,7 +218,8 @@ request 401(需登录接口)
188
 
218
 
189
 - [ ] `GET /api/member/serviceAgreement` 返回开关与正文
219
 - [ ] `GET /api/member/serviceAgreement` 返回开关与正文
190
 - [ ] `POST /api/member/register` 成功 → 登录页预填手机号
220
 - [ ] `POST /api/member/register` 成功 → 登录页预填手机号
191
-- [ ] `POST /api/member/login` 手机号 / 会员名称均可登录
221
+- [ ] `POST /api/member/login` 返回 **根级** `token`、`memberId`;前端能登录并进首页
222
+- [ ] 手机号 / 会员名称(会员 ID)均可登录
192
 - [ ] 协议未勾选时注册/登录被后端拒绝且 Toast 文案正确
223
 - [ ] 协议未勾选时注册/登录被后端拒绝且 Toast 文案正确
193
 - [ ] 登录后 `GET /api/member/profile` 我的页展示昵称/手机号
224
 - [ ] 登录后 `GET /api/member/profile` 我的页展示昵称/手机号
194
 - [ ] 商品详情加购无 Token 跳转登录页
225
 - [ ] 商品详情加购无 Token 跳转登录页
@@ -202,6 +233,7 @@ request 401(需登录接口)
202
 |----|------|
233
 |----|------|
203
 | 短信验证码 | 不做 |
234
 | 短信验证码 | 不做 |
204
 | 忘记密码 / 换绑手机 | 不做 |
235
 | 忘记密码 / 换绑手机 | 不做 |
236
+| 以数字 `member_id` 登录 | 不做 |
205
 | 资料编辑 / 地址管理 | 另册;接口已存在 `/profile`、`/address/**` |
237
 | 资料编辑 / 地址管理 | 另册;接口已存在 `/profile`、`/address/**` |
206
 | 微信 OAuth | 不做 |
238
 | 微信 OAuth | 不做 |
207
 
239
 
@@ -212,7 +244,8 @@ request 401(需登录接口)
212
 | 版本 | 说明 |
244
 | 版本 | 说明 |
213
 |------|------|
245
 |------|------|
214
 | **v1.0** | 首版:会员登录改造、注册分包页、协议组件、我的页入口 |
246
 | **v1.0** | 首版:会员登录改造、注册分包页、协议组件、我的页入口 |
247
+| **v1.1** | 对齐后端 v1.3:登录 **根级** `token`/`memberId` 解析;登录文案「会员 ID」;联调清单补充 |
215
 
248
 
216
 ---
249
 ---
217
 
250
 
218
-*文档版本:v1.0 · 关联《会员注册登录技术方案.md》v1.2*
251
+*文档版本:v1.1 · 关联《会员注册登录技术方案.md》v1.3*

+ 1 - 1
doc/消费者APP/会员注册登录/会员注册登录技术方案.md

@@ -303,7 +303,7 @@ MemberAppServiceImpl.login
303
 | `POST /login`(account:手机号/会员名称) | **已实现** |
303
 | `POST /login`(account:手机号/会员名称) | **已实现** |
304
 | `BizMemberMapper.selectByMemberCode` | **已实现** |
304
 | `BizMemberMapper.selectByMemberCode` | **已实现** |
305
 | `POST /sms/send` | **保留代码、非本期** |
305
 | `POST /sms/send` | **保留代码、非本期** |
306
-| C 端注册/登录 **前端** | **已实现**(见《会员注册登录前端技术方案.md》) |
306
+| C 端注册/登录 **前端** | **已实现**(见《会员注册登录前端技术方案.md》v1.1,含根级 token 解析) |
307
 
307
 
308
 ---
308
 ---
309
 
309
 

+ 4 - 1
shop-app/api/member.js

@@ -10,7 +10,10 @@ export function memberRegister(data) {
10
   })
10
   })
11
 }
11
 }
12
 
12
 
13
-/** 会员登录(匿名) */
13
+/**
14
+ * 会员登录(匿名)
15
+ * 成功响应与平台 /login 一致:根级 token、memberId(非 data.token)
16
+ */
14
 export function memberLogin(data) {
17
 export function memberLogin(data) {
15
   return request({
18
   return request({
16
     url: '/api/member/login',
19
     url: '/api/member/login',

+ 7 - 9
shop-app/components/account/AgreementBlock.vue

@@ -78,8 +78,8 @@ function openPopup() {
78
 .agreement-card {
78
 .agreement-card {
79
 	margin-top: 24rpx;
79
 	margin-top: 24rpx;
80
 	// padding: 20rpx 24rpx;
80
 	// padding: 20rpx 24rpx;
81
-	background: #f5faf6;
82
-	border: 1rpx solid #e8f5e9;
81
+	// background: #f5faf6;
82
+	// border: 1rpx solid #e8f5e9;
83
 	border-radius: 12rpx;
83
 	border-radius: 12rpx;
84
 	box-sizing: border-box;
84
 	box-sizing: border-box;
85
 }
85
 }
@@ -90,16 +90,14 @@ function openPopup() {
90
 }
90
 }
91
 
91
 
92
 .agreement-card__box {
92
 .agreement-card__box {
93
-	width: 36rpx;
94
-	height: 36rpx;
95
-	margin-top: 2rpx;
96
-	margin-right: 16rpx;
97
-	border: 2rpx solid #a5d6a7;
98
-	border-radius: 8rpx;
93
+	width: 32rpx;
94
+	height: 32rpx;
95
+	margin-right: 12rpx;
96
+	border: 2rpx solid #c8e6c9;
97
+	border-radius: 6rpx;
99
 	display: flex;
98
 	display: flex;
100
 	align-items: center;
99
 	align-items: center;
101
 	justify-content: center;
100
 	justify-content: center;
102
-	flex-shrink: 0;
103
 	background: #fff;
101
 	background: #fff;
104
 }
102
 }
105
 
103
 

+ 2 - 2
shop-app/pages/login/index.vue

@@ -10,7 +10,7 @@
10
 		<view class="auth-body">
10
 		<view class="auth-body">
11
 			<view class="auth-card">
11
 			<view class="auth-card">
12
 				<text class="auth-card__title">会员登录</text>
12
 				<text class="auth-card__title">会员登录</text>
13
-				<text class="auth-card__sub">可使用手机号或会员名称登录</text>
13
+				<text class="auth-card__sub">可使用手机号或会员名称(会员 ID)登录</text>
14
 
14
 
15
 				<view class="auth-field">
15
 				<view class="auth-field">
16
 					<view class="auth-field__box">
16
 					<view class="auth-field__box">
@@ -19,7 +19,7 @@
19
 							v-model="form.account"
19
 							v-model="form.account"
20
 							class="auth-field__input"
20
 							class="auth-field__input"
21
 							type="text"
21
 							type="text"
22
-							placeholder="手机号或会员名称"
22
+							placeholder="手机号或会员名称(会员 ID)"
23
 							placeholder-class="auth-field__placeholder"
23
 							placeholder-class="auth-field__placeholder"
24
 							confirm-type="next"
24
 							confirm-type="next"
25
 						/>
25
 						/>

+ 6 - 3
shop-app/store/user.js

@@ -30,14 +30,17 @@ export function useUserStore() {
30
     const password = payload.password
30
     const password = payload.password
31
     const agreementAccepted = !!payload.agreementAccepted
31
     const agreementAccepted = !!payload.agreementAccepted
32
     return memberLogin({ account, password, agreementAccepted }).then((res) => {
32
     return memberLogin({ account, password, agreementAccepted }).then((res) => {
33
-      const data = res.data || {}
34
-      const token = data.token
33
+      // v1.3:与平台 /login 一致,token、memberId 在响应根级;兼容旧版 data 包裹
34
+      const token = res.token || (res.data && res.data.token)
35
+      const memberId = res.memberId != null && res.memberId !== ''
36
+        ? res.memberId
37
+        : (res.data && res.data.memberId)
35
       if (!token) {
38
       if (!token) {
36
         return Promise.reject(new Error('登录失败,未返回令牌'))
39
         return Promise.reject(new Error('登录失败,未返回令牌'))
37
       }
40
       }
38
       setToken(token)
41
       setToken(token)
39
       state.token = token
42
       state.token = token
40
-      state.memberId = data.memberId || ''
43
+      state.memberId = memberId != null ? memberId : ''
41
       return res
44
       return res
42
     })
45
     })
43
   }
46
   }

+ 1 - 1
shop-app/styles/auth.scss

@@ -1,7 +1,7 @@
1
 @import '@/styles/morandi.scss';
1
 @import '@/styles/morandi.scss';
2
 
2
 
3
 .auth-page {
3
 .auth-page {
4
-	min-height: 100%;
4
+	height: 100vh;
5
 	display: flex;
5
 	display: flex;
6
 	flex-direction: column;
6
 	flex-direction: column;
7
 	background: $morandi-bg-page;
7
 	background: $morandi-bg-page;

+ 1 - 1
shop-app/subpackage/account/register.vue

@@ -64,7 +64,7 @@
64
 								v-model="form.memberCode"
64
 								v-model="form.memberCode"
65
 								class="auth-field__input"
65
 								class="auth-field__input"
66
 								type="text"
66
 								type="text"
67
-								placeholder="会员名称(选填,不填自动生成)"
67
+								placeholder="会员名称/会员 ID(选填,不填自动生成)"
68
 								placeholder-class="auth-field__placeholder"
68
 								placeholder-class="auth-field__placeholder"
69
 							/>
69
 							/>
70
 						</view>
70
 						</view>