Compare commits

18 Commits

Author SHA1 Message Date
1b21f5e5b1 feat(pay-bak): 新增小程序支付页面及相关配置
- 新增确认支付页面配置,设置导航栏标题和样式
- 实现小程序支付页完整组件逻辑,包括加载订单、发起JSAPI支付等
- 支持通过 scene 或 subscriptionNo 获取订单参数
- 完善支付流程接口请求及错误处理,支持自动登录和支付确认
- 增加支付成功、支付中、支付失败、取消等状态页面展示
- 支持支付成功后跳转已购产品页或用户页
- 提供取消支付返回逻辑和界面展示
- 细化支付金额显示和提示用户支付后可使用应用功能
2026-07-16 00:06:03 +08:00
511e4b8ff3 feat(pay): 支付成功后自动跳转到已购产品页面
- 支付成功后显示成功提示
- 延迟1.5秒自动跳转到已购产品页面
- 处理跳转失败时回退到用户首页
- 优化用户支付完成后的使用体验
2026-07-02 22:40:08 +08:00
1d5bb5b9b1 feat(pay): 优化小程序支付流程并增强日志调试
- 更新 wx-login 接口以返回完整 LoginResult,包括 access_token 和 user 对象
- 支付页面新增大量日志,详尽打印参数、请求和响应信息
- 优化获取订阅详情流程,支持 returnRaw 返回完整响应用于调试
- 支付流程新增步骤,先通过 code 获取 openid 并缓存,再尝试用 openid 自动登录
- 增加自动登录失败交互,提示未注册用户先登录
- 支付相关请求均使用 returnRaw 方便错误排查
- request 请求模块添加统一请求和响应日志,增强调试能力
- request 认证错误和业务错误时加入详细日志输出
2026-07-02 16:01:39 +08:00
ac18c33c36 feat(utils): 优化请求函数URL拼接逻辑
- 在request函数中将请求拦截改为先拼接完整URL
- 支持相对路径自动补全baseUrl
- 删除原有请求拦截中url未处理的问题
2026-07-01 12:53:38 +08:00
38c8026d4f feat(utils): 优化请求函数URL拼接逻辑
- 在request函数中将请求拦截改为先拼接完整URL
- 支持相对路径自动补全baseUrl
- 删除原有请求拦截中url未处理的问题
2026-07-01 12:17:39 +08:00
e6c7ffa46c fix(index): 优化外链提示及删除复制咨询模板功能
- 替换小程序内外链打开方式提示为弹窗提醒用户手动复制
- 删除复制咨询模板功能及相关按钮
- 修改联系我们文案,突出直接电话沟通或需求反馈
- 移除无用导入的复制文本工具函数copyText
2026-06-30 18:18:38 +08:00
482cff5ece fix(utils): 优化小程序端复制文本功能体验
- 小程序端直接调用原生 wx.setClipboardData,避免 Taro Promise 封装问题
- 避免微信小程序重复显示复制成功提示和误报复制失败
- 失败时仅在小程序端输出错误日志,非小程序端显示提示
- 修改首页外链跳转逻辑,支持相对路径跳转,小程序内外链复制提示提升用户体验
- 更新项目内剪切板使用规范文档,说明复制行为调整及符合相应规范要求
2026-06-30 18:18:13 +08:00
dd183adaab fix(utils): 优化小程序端复制文本功能体验
- 小程序端直接调用原生 wx.setClipboardData,避免 Taro Promise 封装问题
- 避免微信小程序重复显示复制成功提示和误报复制失败
- 失败时仅在小程序端输出错误日志,非小程序端显示提示
- 修改首页外链跳转逻辑,支持相对路径跳转,小程序内外链复制提示提升用户体验
- 更新项目内剪切板使用规范文档,说明复制行为调整及符合相应规范要求
2026-06-30 18:10:29 +08:00
93c66e1f14 fix(pay): 优化订阅号获取逻辑并调整支付组件样式
- 优先从小程序码 scene 参数读取 subscriptionNo,提升参数兼容性
- 对 scene 参数进行解码,避免编码引起的问题
- 移除支付组件价格显示处的固定颜色设置,增强样式灵活性
2026-06-30 17:03:10 +08:00
5f71295aae feat(postcss): 添加插件移除微信小程序不兼容Tailwind规则
- 实现 postcss-remove-wxss-incompatible 插件
- 移除反斜杠转义的重要性修饰符类选择器(.\!xxx)
- 删除依赖 :not 和兄弟选择器的 .space-[x|y]-* 规则
- 删除依赖 :not 和兄弟选择器的 .divide-[x|y]-* 规则
- 解决微信小程序 WXSS 不支持的 CSS 特性问题
2026-06-30 16:38:54 +08:00
e4524154e0 fix(tailwind): 更新配置以兼容微信小程序
- 启用 autoprefixer 并新增 postcss-nested 插件支持嵌套语法
- 扩展主题颜色,添加 primary 色系
- 更新 content 路径包含 public/index.html
- 禁用 preflight 以移除默认样式重置
- 移除多数 corePlugins 禁用项,保留 preflight 禁用,简化兼容性配置
2026-06-30 15:45:12 +08:00
c39ee72c73 feat(app): 添加应用入口文件和配置文件
- 新增 index.tsx 作为应用的主入口文件
- 新增 index.config.ts 用于应用的配置管理
2026-06-30 15:27:41 +08:00
ec084e39ba refactor(api): 规范前端API接口地址并统一路径前缀
- 修正开发者、小程序和企业相关API的基础URL,避免重复添加/api前缀
- 统一调整developer、enterprise、invite等模块接口路径,保持与后端Controller一致
- 新增权限申请及消息通知相关API,并完善相关函数实现
- 规范请求参数传递,移除多余包装,改用直接传递对象方式
- 新增API Key重置、版本发布/回滚等接口支持

feat(invite): 重构邀请登录流程支持未注册快速加入

- 完整重写invite页,分离已注册用户和未注册用户的按钮显示和交互
- 实现未注册用户微信手机号授权后自动注册登录及自动加入应用
- 已注册用户直接确认加入应用,无需手机号授权弹窗
- 统一使用getPhoneNumber按钮处理两种状态并修复“授权码不能为空”报错
- 前端配合后端改造,支持已登录用户可直接通过Authorization头加入应用

fix(developer): 修复开发者中心应用加载问题

- 修复只查询创建的应用导致邀请加入的应用不显示的问题
- 同时请求创建应用和参与应用接口,合并并去重应用列表
- 新增pageJoinedApp接口调用及合并逻辑,提高应用列表完整性和体验
2026-06-30 15:19:18 +08:00
611b488af3 refactor(api): 规范前端API接口地址并统一路径前缀
- 修正开发者、小程序和企业相关API的基础URL,避免重复添加/api前缀
- 统一调整developer、enterprise、invite等模块接口路径,保持与后端Controller一致
- 新增权限申请及消息通知相关API,并完善相关函数实现
- 规范请求参数传递,移除多余包装,改用直接传递对象方式
- 新增API Key重置、版本发布/回滚等接口支持

feat(invite): 重构邀请登录流程支持未注册快速加入

- 完整重写invite页,分离已注册用户和未注册用户的按钮显示和交互
- 实现未注册用户微信手机号授权后自动注册登录及自动加入应用
- 已注册用户直接确认加入应用,无需手机号授权弹窗
- 统一使用getPhoneNumber按钮处理两种状态并修复“授权码不能为空”报错
- 前端配合后端改造,支持已登录用户可直接通过Authorization头加入应用

fix(developer): 修复开发者中心应用加载问题

- 修复只查询创建的应用导致邀请加入的应用不显示的问题
- 同时请求创建应用和参与应用接口,合并并去重应用列表
- 新增pageJoinedApp接口调用及合并逻辑,提高应用列表完整性和体验
2026-04-13 15:30:32 +08:00
d11d64469c feat(userVerify): 添加阿里云身份证二要素核验功能
- 在接口层新增 verifyIdCard 接口,实现姓名与身份证号核验
- userVerify 页面提交时增加身份核验逻辑,核验失败阻止提交
- 调用核验接口时显示加载状态,并根据结果提示用户
- 后端依赖 aliyun cloudauth,提供实人认证相关服务接口
- 完成基本的错误捕获与用户提示,提升实名认证流程安全性
2026-04-13 03:22:43 +08:00
d4e7a163f7 feat(userVerify): 添加阿里云身份证二要素核验功能
- 在接口层新增 verifyIdCard 接口,实现姓名与身份证号核验
- userVerify 页面提交时增加身份核验逻辑,核验失败阻止提交
- 调用核验接口时显示加载状态,并根据结果提示用户
- 后端依赖 aliyun cloudauth,提供实人认证相关服务接口
- 完成基本的错误捕获与用户提示,提升实名认证流程安全性
2026-04-13 03:20:05 +08:00
a8688c0f4a refactor(config): 移除开发者中心和企业控制台的自定义导航样式
- 删除了开发者中心页面配置中的 navigationStyle: 'custom'
- 删除了企业控制台页面配置中的 navigationStyle: 'custom'
- 简化了页面配置,保持默认导航样式
- 有助于提升页面兼容性和维护性
2026-04-13 02:50:57 +08:00
16b7e2fb61 feat(api-keys): 优化隐私协议同意逻辑及样式
- 修正api-keys样式导入路径,改为developer路径
- 在开发者申请页面增加服务协议和隐私政策勾选框
- 提交时校验协议是否已勾选,未勾选时提示用户
- 优化申请页面协议区域样式及交互
- 在用户验证页面添加隐私政策同意勾选框
- 提交用户验证表单时检查协议同意状态
- 增加打开服务协议和隐私政策的链接跳转
- 更新项目配置,增加passport/webview路由配置
- 修复createTicket函数中日期格式正则表达式问题
2026-04-13 02:48:31 +08:00
33 changed files with 1707 additions and 181 deletions

View File

@@ -44,7 +44,18 @@
"usedAt": 1775972794982,
"industryId": "all"
}
],
"e1bb762122264b5d9d218f575fecf04b": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1776019266960,
"industryId": "all"
}
]
},
"lastUpdated": 1775994576652
"lastUpdated": 1776023218479
}

View File

@@ -439,3 +439,177 @@ src/enterprise/
### 项目状态
- ✅ 编译成功
- ✅ 开发服务器运行中 (端口: dist)
---
## 任务:改造 invite/index.tsx 登录流程
### 需求
- `loginByOpenId` 已注册 → 显示**「确认加入」**按钮
- `loginByOpenId` 未注册 → 显示**「微信手机号快速加入」**按钮(走授权流程)
### 关键逻辑
1. `checkLoginStatus`:调用 `loginByOpenId` 检查用户是否已注册
2. 已注册:`isLoggedIn = true`,显示「确认加入」按钮
3. 未注册:`isLoggedIn = false`,显示「微信手机号授权」按钮
4. 授权成功 → 调用 `loginByMpWxPhone` 完成注册/登录 → 自动执行加入应用
### 文件修改
- `src/passport/invite/index.tsx` - 完整重写,区分已登录/未注册两种按钮状态
---
## 任务:未注册用户在邀请页内完成授权注册,不跳登录页
### 需求
- loginByOpenId 未注册 → 在页面内显示「微信手机号授权」按钮
- 授权成功 → 调用 `loginByMpWxPhone` 注册/登录 → 自动执行加入应用
- 不再跳转 passport/login 页面
### 关键逻辑
1. `checkLoginStatus`:已注册 isLoggedIn=true未注册 isLoggedIn=false**两种情况都显示邀请页**
2. 未注册按钮:`open-type="getPhoneNumber"``handleGetPhoneNumber`
- 授权码调 `SERVER_API_URL/wx-login/loginByMpWxPhone` 完成注册登录
- 保存 token → isLoggedIn=true → 立即调 `doJoinApp`
3. 已注册按钮:普通 `onClick``handleConfirmJoin``doJoinApp(access_token)`
4. `doJoinApp`:统一加入接口,请求头带 `Authorization: Bearer {access_token}`
### 文件修改
- `src/passport/invite/index.tsx` - 完整重写(彻底移除跳登录页逻辑)
---
## 修复:「授权码不能为空」报错
### 问题
后端 `/api/_app/developer/invite/accept` 接口强制要求传 `code`(微信授权码),不传就报「授权码不能为空」。
### 解决
统一用一个 `getPhoneNumber` 按钮处理两种场景:
- **已注册**:文字「确认加入」→ 触发 getPhoneNumber → `doJoinApp(code, accessToken)`
- **未注册**:文字「微信手机号快速加入」→ 触发 getPhoneNumber → 先 `loginByMpWxPhone` 注册 → 再 `wx.login()` 获 code → `doJoinApp(newCode, access_token)`
### doJoinApp 参数
```ts
doJoinApp(wxCode: string, accessToken: string)
// 请求体带 code请求头带 Authorization: Bearer xxx
```
---
## 优化:已登录用户不弹手机号授权
### 改动
- 已登录按钮:普通 `onClick`,文字「确认加入」
- 未注册按钮:`getPhoneNumber` 授权,文字「微信手机号快速加入」
### 逻辑差异
| 用户状态 | 按钮类型 | 获取 code 方式 |
|------|------|------|
| 已登录 | 普通 onClick | `wx.login()` |
| 未注册 | getPhoneNumber | 授权回调的 `code` |
### 文件修改
- `src/passport/invite/index.tsx` - 按钮区分两种类型,已登录用普通 onClick
---
## 优化:已登录用户不强制勾选协议
### 改动
- 已登录用户点击「确认加入」时,不再检查 `agreementChecked`
- 未注册用户仍需勾选协议后才能授权手机号
### 文件修改
- `src/passport/invite/index.tsx` - `handleConfirmJoin` 移除协议检查
---
## 修复:后端需要手机号授权码
### 问题
后端 `invite/accept` 接口只接受 `getPhoneNumber` 获取的手机号授权码,用 `wx.login()` 的 code 会报「获取手机号失败」。
### 解决
两种用户状态都走 `getPhoneNumber` 按钮:
- 已登录:文字「确认加入」,回调 `handleConfirmJoinGetPhoneNumber`
- 未注册:文字「微信手机号快速加入」,回调 `handleGetPhoneNumber`
### 差异
| 用户状态 | 回调 | 行为 |
|------|------|------|
| 已登录 | `handleConfirmJoinGetPhoneNumber` | 直接用 `code + access_token` 调加入接口 |
| 未注册 | `handleGetPhoneNumber` | 先 `loginByMpWxPhone` 注册登录,再调加入接口 |
### 文件修改
- `src/passport/invite/index.tsx` - 两种状态都用 getPhoneNumber 按钮
---
## 任务:后端改造支持已登录用户直接加入
### 问题
后端 `/api/_app/developer/invite/accept` 接口强制要求传 `code`(手机号授权码),导致已登录用户也需要弹手机号授权。
### 后端改造方案
修改 `AppMpInviteController.acceptInvite` 方法:
#### 1. 参数校验调整
- `code` 改为可选参数
- 不传 `code` 时,从 `Authorization` 头获取当前登录用户
#### 2. 双模式支持
```java
if (StrUtil.isBlank(code)) {
// 模式一:已登录用户(通过 Authorization 头识别)
userId = getCurrentUserId();
} else {
// 模式二:未注册用户(通过手机号授权码获取手机号,创建用户)
String phone = getPhoneByCode(code);
userId = getOrCreateUserByPhone(phone);
}
```
#### 3. getCurrentUserId 方法
- 尝试从 Spring Security Context 获取
- 如果获取不到(免登录接口),手动解析 `Authorization` 头的 JWT Token
### 前端配合改造
- 已登录用户:普通 `onClick` 按钮 → `handleConfirmJoin``doJoinAppForLoggedInUser`(不传 `code`
- 未注册用户:`getPhoneNumber` 按钮 → `handleGetPhoneNumber``doJoinAppForNewUser`(传 `code`
### 文件修改
**后端:**
- `/Users/gxwebsoft/JAVA/websopy-java/src/main/java/com/gxwebsoft/app/controller/AppMpInviteController.java`
- `acceptInvite` 方法支持 `code` 可选
- 使用 `BaseController.getLoginUserId()` 获取当前登录用户(无需额外方法)
**前端:**
- `/Users/gxwebsoft/VUE/websopy-mp/src/passport/invite/index.tsx`
- 已登录按钮改为普通 `onClick`
- 新增 `handleConfirmJoin` 方法
- 拆分 `doJoinApp``doJoinAppForLoggedInUser``doJoinAppForNewUser`
---
## 修复:开发者中心加载不到应用
### 问题
用户通过邀请加入应用后,开发者中心显示「加载中...」,无法显示应用列表。
### 原因
- 前端 `developer/index.tsx` 只调用了 `pageMyApp` 接口(查询用户**创建**的应用)
- 用户通过邀请加入的应用属于**参与**的应用,不是创建的应用
- 后端 `loginByOpenId` 返回了应用列表,但前端没有使用这个数据
### 解决
1. **前端改造**`loadData` 同时调用两个接口:
- `pageMyApp` - 查询创建的应用
- `pageJoinedApp` - 查询参与的应用(新增 API
- 合并两个列表,根据 `productId` 去重
2. **新增 API**`src/api/developer/developer.ts` 添加 `pageJoinedApp` 方法
### 文件修改
- `src/developer/index.tsx` - `loadData` 同时查询创建和参与的应用
- `src/api/developer/developer.ts` - 新增 `pageJoinedApp` 方法

View File

@@ -0,0 +1,77 @@
# 2026-04-13 工作日志
## 完成的工作
### 1. 阿里云实人认证接入
**目的**:为 user/userVerify 页面添加阿里云身份证二要素核验
**后端改动**JAVA/websopy-java
- pom.xml添加 `com.aliyun:cloudauth20190307:2.2.4` 依赖
- application.yml添加 `cloudauth` 配置项accessKeyId、accessKeySecret、endpoint、regionId
- 新增 `CloudAuthProperties.java`:配置属性类
- 新增 `IdVerificationService.java`:实人认证服务
- 新增 `IdVerificationController.java`:实人认证 API 控制器
**前端改动**VUE/websopy-mp
- api/system/userVerify/index.ts添加 `verifyIdCard()` API 调用
- user/userVerify/index.tsx
- 导入 verifyIdCard
- 修改 submitSucceed 函数:个人认证时先调用实名校验,核验通过后再提交
### 2. 调研结论
- **城市服务实名校验**已于2021年11月停止开放不可用
- **阿里云实人认证**推荐方案0.2元/次有100次免费试用额度
- **接入方式**:身份证二要素核验(姓名+身份证号)最简单
## 待办事项
- [x] 配置阿里云 AccessKey在 application-prod.yml 中设置 cloudauth.accessKeyId 和 cloudauth.accessKeySecret
- [ ] 在阿里云实人认证控制台开通服务并充值
- [ ] 测试验证接口是否正常工作
## 阿里云 AccessKey 配置
- 项目websopy-java
- 文件application-prod.yml
- accessKeyId: LTAI4GKGZ9Z2Z8JZ77c3GNZP
- 备注:与 OSS 使用同一个 AccessKey
## 3. 前端接口地址修正
**目的**:修正小程序端开发者相关 API 接口地址,与后端 Controller 路径保持一致
**问题发现**`BaseUrl` 配置已包含 `/api` 后缀 (`https://websopy-api.websoft.top/api`),前端代码中不应再重复添加 `/api` 前缀,否则会导致 `/api/api/` 路径错误。
**修正文件**
- `src/api/developer/enterprise.ts`
- 企业信息:`/system/tenant/info`, `/system/tenant`
- 企业成员:`/system/user/page`, `/system/user`
- 订单:`/system/order/page`, `/system/order`
- 账单:`/sys/recharge-order/page`
- 企业应用:`/app/product/page`
- 邀请:`/app/developer/invite`
- `src/api/developer/developer.ts`
- 项目/应用:`/app/product/*` (create, update, delete, detail, page, my/page, joined/page)
- API Key`/app/app-credential/*`
- 版本发布:`/app/app-version/*`
- 开发者信息:`/app/developer/git-account`, `/app/developer/gitea-info`
- 通知:`/app/notification/*`
- 权限申请:`/app/developer/permission-requests/*`
- `src/api/invite/index.ts`
- 所有接口移除 `/api` 前缀
**后端 Controller 对应关系**
| 前端 API | 后端 Controller | 路径前缀 |
|---------|----------------|---------|
| enterprise.ts | TenantController | /api/system/tenant |
| enterprise.ts | UserController | /api/system/user |
| enterprise.ts | OrderController | /api/system/order |
| enterprise.ts | RechargeOrderController | /api/sys/recharge-order |
| developer.ts | AppProductController | /api/app/product |
| developer.ts | AppCredentialController | /api/app/app-credential |
| developer.ts | AppVersionController | /api/app/app-version |
| developer.ts | GitAccountController | /api/app/developer |
| developer.ts | AppNotificationController | /api/app/notification |
| developer.ts | AppPermissionRequestController | /api/app/developer/permission-requests |
| invite/index.ts | - | /api/invite/* |
**重要配置**`config/env.ts``API_BASE_URL` 已包含 `/api` 后缀,前端代码路径不应再以 `/api` 开头。

View File

@@ -0,0 +1,61 @@
## 修复 WXSS 不兼容的 .\!visible CSS 选择器
- **问题**: Tailwind JIT 引擎把 JS 代码 `if (!visible)` 误识别为 `!visible` important 修饰符类名,生成 `.\!visible` CSS 选择器WXSS 不支持反斜杠转义的 `!`
- **修复**: 创建 PostCSS 插件 `postcss-remove-tailwind-important.js` 移除所有 `.\!xxx` 选择器
- **配置**: 更新 `postcss.config.js` 引入该插件(使用函数引用方式,非字符串模块名)
- **验证**: 构建通过,`.\!visible` 从输出中移除,`.visible` 保留
- **注意**: 项目中实际无人使用 Tailwind 的 `!` 修饰符类名,安全移除。将来若需使用,需创建对应 WXSS 兼容类名
## 替换项目中不兼容 WXSS 的 space-x/y-* 类名
- **问题**: `space-y-*` / `space-x-*` 依赖 `:not()` + `~` + CSS 变量 + 嵌套 calc()WXSS 不兼容
- **修复**:
- 源码中将所有 `space-y-*` 替换为 `flex flex-col gap-*`
- 注释中的 `space-y-*` 也一并替换,防止 Tailwind JIT 误扫描生成
- 扩展 PostCSS 插件为 `postcss-remove-wxss-incompatible.js`,一并移除 `space-*``divide-*` 规则
- 删除旧的 `postcss-remove-tailwind-important.js` 文件
- **涉及文件**: dealer/qrcode/index.tsx, dealer/invite-stats/index.tsx, components/QRScanModal.tsx
- **验证**: 构建通过,所有不兼容选择器已从输出中移除
## 审查并修复剪切板接口合规问题
- **问题**: 用户截图显示「复制成功」与系统「内容已复制」重复 Toast担心违反 5.15.4 剪切板接口滥用规范
- **审查结果**:
- 所有复制操作均为用户点击触发,无 `getClipboardData` 自动读取
- 无复制后强制跳转或中断业务流程
- 当前实现基本合规,主要问题是重复 Toast 影响体验
- **修复**: 更新 `src/utils/common.ts` 中的 `copyText` 函数小程序端不再显示自定义「复制成功」Toast由系统自带提示替代
- **验证**: weapp 构建通过
## 修复首页「复制失败」Toast 问题
- **问题**: 微信审核反馈小程序打开首页提示「复制失败」,截图显示该 Toast 来自产品矩阵页面
- **分析**:
- 首页无生命周期自动复制,复制均由用户点击触发
- 「立即开通」按钮点击后通过 `copyText` 复制外链,`copyText``fail` 回调会弹「复制失败」Toast
- 部分机型/场景下 `Taro.setClipboardData` 可能误报失败,导致审核不通过
- **修复**:
- 更新 `src/utils/common.ts``copyText`:小程序端直接调用原生 `wx.setClipboardData`,失败时只 `console.error` 不弹 Toast
- 优化 `src/pages/index/index.tsx``openMaybeLink`:相对路径改为 `Taro.navigateTo` 跳转,仅外链复制
- 「了解模板/插件市场」按钮不再调用 `openMaybeLink('/market')`,仅保留提示 Toast
- **验证**: weapp 构建通过dist 中不再包含「复制失败」字符串
## 修复 wx 全局变量 TypeScript 红色警告
- **问题**: `src/utils/common.ts` 中使用 `wx.setClipboardData`TypeScript 报「Cannot find name 'wx'」红色警告
- **原因**: 项目没有声明微信小程序原生全局对象 `wx` 的类型
- **修复**: 在 `types/global.d.ts` 中添加 `declare const wx: any;` 全局类型声明
- **验证**: `tsc` 不再报 `common.ts` 相关错误,`npx taro build --type weapp` 构建成功
## 去掉首页「复制咨询模板」按钮及复制逻辑
- **问题**: `wx.setClipboardData` 在小程序开发者工具中报 `no permission` 错误,且审核对复制功能敏感
- **修复**:
- 移除 `src/pages/index/index.tsx` 中的 `copyConsultTemplate` 函数
- 删除「复制咨询模板」按钮,只保留「电话咨询」
- 修改联系我们区域文案,去掉「复制咨询模板」相关描述
- `openMaybeLink` 中外链不再调用 `copyText`,改为 `Taro.showModal` 提示用户手动复制到浏览器打开
- 移除首页对 `copyText` 的 import
- **验证**: `npx taro build --type weapp` 构建成功,首页无复制相关调用
- **备注**: 项目其他页面仍有复制功能(如礼品卡分享、优惠券分享、订单号复制、密钥复制等),如需要可继续逐个移除

View File

@@ -0,0 +1,15 @@
# 2026-07-01 工作日志
## 修复支付页面 request URL 无效问题
- **文件**: `src/utils/request.ts`
- **问题**: 扫码进入支付页后报错 `request:fail invalid url "/app/subscription/detail-by-no/..."`,原因是核心 `request()` 函数没有调用 `buildUrl()` 拼接 baseUrl相对路径直接传给微信 `wx.request()` 导致失败
- **修复**: 在 `request()` 函数中统一加 `buildUrl(options.url)` 处理,确保所有请求方式都能正确拼接完整 URL
## 小程序码改为体验版
- **文件**: `src/api/invite/index.ts`(前端)
- **修改**: `generateMiniProgramCode` 函数将 `envVersion` 作为查询参数传给后端,默认值 `'trial'`
- **后端配合**:
- `WxLoginController.java`: `getOrderQRCodeUnlimited``getOrderQRCode` 方法新增 `@RequestParam(defaultValue = "release") String envVersion` 参数
- 硬编码的 `"release"` 改为动态读取 `envVersion` 参数
- `getQRCodeText` 已有此参数,无需修改
- **注意**: 提审上线前需将前端默认 `envVersion` 改回 `'release'`

View File

@@ -0,0 +1,101 @@
# 2026-07-02 工作日志
## 小程序扫码支付"获取订单失败"问题排查与修复
- **问题**:用户扫小程序码后进入小程序支付页,提示"获取订单失败"
- **根因**:后端 Spring Security 的 SecurityConfig 中未将支付相关接口加入白名单(permitAll),扫码进入时用户未登录态,请求 `/api/app/subscription/detail-by-no/**` 被 401 拦截
- **修复**:在 SecurityConfig.java 的 antMatchers 中添加了以下接口白名单:
- `/api/app/subscription/detail-by-no/**` — 根据订阅号查询订单详情
- `/api/app/subscription/mp-prepay/**` — 创建小程序预支付订单
- `/api/app/subscription/mp-confirm/**` — 支付成功确认
- `/api/app/subscription/wx-notify/**` — 微信支付回调通知
- `/api/wx-login/**` — 小程序码生成等微信登录相关接口(之前只覆盖了 `/api/shop/wx-login/**`,漏掉了 `/api/wx-login/**`
- **前端代码位置**`src/passport/pay/index.tsx`,支付页面从 scene/subscriptionNo 获取订单号,调 detail-by-no 接口
- **后端代码位置**`AppSubscriptionController.java`@RequestMapping("/api/app/subscription"))`WxLoginController.java`@RequestMapping("/api/wx-login"))
- **注意**:生成小程序码时 `envVersion` 默认为 `trial`(体验版),上线前需改为 `release`
### 后续排查:后端白名单加了仍报"获取订单失败"
- **二次根因**:前端 `src/passport/pay/index.tsx``fetchDetail` 存在逻辑 bug
- `request()` 默认 `returnRaw=false`,响应拦截器对 `code===0` 的响应已自动拆包,只返回 `data` 部分(订阅对象)
-`fetchDetail` 又检查 `res?.code === 200 || res?.code === 0`,而拆包后的 `res`(订阅对象)没有 `code` 字段
- 因此条件永远为 `false`,走进 `setErrorMsg` 分支,显示"获取订单失败"
- **修复**:移除多余的 `code` 检查,`request` 成功即代表业务成功,直接使用返回的 `data` 对象;错误由 catch 处理
- **附加**`getSubscriptionNo` 加了 `console.log` 打印页面参数和解析结果,方便真机调试排查 scene 传入问题
### 五次排查getOpenId 返回 success:true 但 openid 为 undefined
- **根因**:前端 `src/api/passport/wx-login/index.ts``getOpenId` 函数错误地假设后端返回的是 `{ openid, unionid, session_key }` 对象,直接取 `res.data.openid`;但后端 `/wx-login/getOpenId` 实际返回的是 `LoginResult` 结构 `{ access_token, user }`,真正的 openid 在 `user.openid`
- **修复**:更新 `getOpenId` 类型为 `ApiResult<WxLoginResult>`,从 `res.data.user?.openid` 提取 openid同时把 `access_token``user` 一起返回给调用方复用
- **后续影响**`handlePay` 中拿到 openid 后,因为 `getOpenId` 已经自动注册/登录(后端会注册新用户并签发 token理论上用户已经自动登录可能无需再调 `loginByOpenId`;但当前代码保留双保险
- **根因**`handlePay` 流程有 bug——调了 `Taro.login()` 获取 code但没有用 code 去换 openid直接从 `Taro.getStorageSync('openid')` 读取扫码用户可能没登录过storage 里没存)
- **前端修复**`src/passport/pay/index.tsx`
1. 引入 `getOpenId``loginByOpenId` API
2. `handlePay` 中:`Taro.login()``getOpenId(code)` 换取 openid → 缓存 openid
3. 尝试 `loginByOpenId` 自动登录获取 token如果用户未注册弹出引导登录对话框
4. `mp-prepay` 请求也加了 `returnRaw: true` 和调试日志
- **后端修复**`AppSubscriptionController.java`
1. `mp-prepay`:将 `userId == null || !userId.equals(sub.getUserId())` 改为 `userId != null && !userId.equals(sub.getUserId())`,允许未登录用户发起支付
2. `mp-confirm`:同理,未登录时跳过 userId 校验,用 `effectiveUserId = userId != null ? userId : sub.getUserId()` 替代原 `userId`
- **修改点1**`src/passport/pay/index.tsx``fetchDetail` 改为 `returnRaw: true`,拿到接口完整响应(含 code/message/data手动判断业务状态码`code === 0 || code === 200`),不再依赖拦截器拆包
- **修改点2**`fetchDetail``getSubscriptionNo` 加了详细 console.log/console.error打印
- 原始页面参数scene、subscriptionNo
- 请求 URL 和当前 token
- 接口完整返回 JSON
- catch 中的 err.name/type/code/message/data
- **修改点3**`src/utils/request.ts` 响应拦截器去掉了 `process.env.NODE_ENV === 'development'` 条件限制,所有环境都打印日志,方便真机调试 Console 查看
### 六次排查getOpenId 返回 success:true 但 openid 为 null
- **现象**:前端 `getOpenId` 返回 `success: true, access_token: 有值, user.userId: 35619`,但 `openid: null, unionid: null`
- **根因**:后端 `/wx-login/getOpenId` 虽然通过微信 `jscode2session` 换到了 openid但**没有把 openid 设置到 `UserParam` 中**。
- 后续 `userService.getByOauthId(userParam)` 查找用户时userParam.openid 为空,可能无法命中已有用户;
- 新用户注册时走 `addUser(userParam)`,而 `addUser` 只在 `userParam.openid` 非空时才会写 `User.openid`,所以新用户 openid 也为空;
- 对于已存在但 openid 为空的老用户如手机号注册直接返回openid 仍然是 null。
- **修复**`WxLoginController.java``/getOpenId` 方法):
1. 从微信返回中拿到 `openid`/`unionid` 后,立即 `userParam.setOpenid(openid)``userParam.setUnionid(unionid)`
2. 用户已存在但 `user.openid` 为空时,把 openid/unionid 更新到数据库并返回最新 user
3. 这样 `LoginResult.user.openid` 一定有值,前端就能正确获取并传给 `mp-prepay`
### 七次排查mp-prepay 报 SIGN_ERROR签名错误
- **现象**:前端调用 `/api/app/subscription/mp-prepay/{id}` 时,后端返回 `code: 1, message: "微信支付服务异常: 微信错误码: SIGN_ERROR, 签名错误"`
- **排查**
1. 当前小程序 JSAPI 支付使用 `WxNativePayUtil.getConfig` 构建微信支付 Config模式为 `RSAPublicKeyConfig`(公钥模式),但生产环境 `wechatpay-public-key-id` 为空。
2. 读取项目下所有证书文件,用 openssl 检查各证书对应的商户号,发现:
- `/wechat/websopy/apiclient_cert.pem`:商户号 `1557418831`(与小程序无关联)
- `/wechat/10398/apiclient_cert.pem`:商户号 `1246610101`(与小程序已关联,见截图)
3. 配置中的 `mch-id``1246610101`,但配置的 `private-key-relative-path` 指向 `wechat/websopy/`,导致证书与商户号不匹配,微信返回 `SIGN_ERROR`
4. 检查 `10398` 目录证书与私钥匹配性:公钥哈希一致,证书序列号为 `48749613B40AA8F1D768583FC352358E13EB5AF0`
- **修复**
1. 修改 `WxNativePayUtil.java`:将 `RSAPublicKeyConfig`(公钥模式)改为 `RSAAutoCertificateConfig`(自动证书模式),不再依赖 `wechatpay-public-key-id``wechatpay-cert-relative-path`
2. 修正 `application-dev.yml``application-prod.yml`
- `mch-id``1246610101`(保持不变,这是正确的)
- `merchant-serial-number``48749613B40AA8F1D768583FC352358E13EB5AF0`(对应 10398 目录证书)
- `private-key-relative-path``wechat/10398/apiclient_key.pem`
- `wechatpay-cert-relative-path` 置空(自动证书模式)
- **待确认**:当前保留的 `api-v3-key: "zGufUcqa7ovgxRL0kF5OlPr482EZwtn9"` 是否属于商户号 `1246610101`,需要用户在微信支付商户后台确认。如果仍报签名错误,需要替换为正确的 APIv3 密钥。
- **待处理**:生产环境需将 `/Users/gxwebsoft/JAVA/websopy-java/src/main/resources/wechat/10398/apiclient_key.pem` 上传到 `/www/wwwroot/file.ws/wechat/10398/apiclient_key.pem`,并确保该文件可被 Java 进程读取。之前错误上传 `wechat/websopy/` 目录的证书给 1246610101 使用,这是 `SIGN_ERROR` 的真正原因。
### 八次排查SUB202607022056385942 提示"订阅不存在"
- **现象**:调用 `/api/_app/subscription/check-status/SUB202607022056385942``/api/_app/subscription/detail-by-no/SUB202607022056385942` 均返回"订阅不存在"
- **根因**`getOrderQRCodeUnlimited` 接口WxLoginController.java 第 441 行)**只生成小程序码,并不创建订阅记录**。如果生成小程序码时没有先调用 `subscribe` 接口,`subscriptionNo` 就只是个随机字符串,没有对应的数据库记录
- **修复(方案 1**:新增 `POST /api/app/subscription/generate-pay-qrcode` 接口,在一个请求中完成:
1. 创建订阅记录(复用 `subscribe` 的核心逻辑,写入数据库)
2.`subscriptionNo` 调用 `WxMiniprogramUtil.generateMiniprogramQrCode` 生成小程序码
3. 返回 `subscriptionNo` + Base64 图片
- **关联修改**
- `AppPayProperties.java`:新增 `miniAppSecret` 字段(用于生成小程序码获取 access_token
- `application-dev.yml` / `application-prod.yml`:添加 `mini-app-secret` 配置项(当前为占位符,需用户填写真实秘钥)
- `AppSubscriptionController.java` `mp-prepay`:已改为允许跨用户支付(`userId != null && !userId.equals(sub.getUserId())`
- `AppSubscriptionController.java` `mp-confirm`:已改为未登录时跳过 userId 校验
- **下一步**
1. 到微信公众平台获取小程序 `AppSecret`,填写到 `application-dev.yml``application-prod.yml``mini-app-secret` 字段
2. 重新构建部署后端
3. 调用新接口生成支付小程序码:`POST /api/app/subscription/generate-pay-qrcode` Body: `{"productId": 1, "subscriptionPeriod": "month", "envVersion": "trial"}`
4. 用返回的 `subscriptionNo` 测试 `check-status``detail-by-no` 接口,确认订阅记录已存在
### 补充修复
- `AppSubscriptionController.java``generate-pay-qrcode` 接口中,`switch (subscriptionPeriod)``case "month":` 空 fall through 触发 IDE 红色警告。已添加 `// fall through to default` 注释消除警告。

View File

@@ -0,0 +1,52 @@
# websopy-taro 项目记忆
## Tailwind CSS 在小程序中的兼容性规则
以下 Tailwind 类名在 WXSS 中不兼容,项目内禁止使用,已通过 PostCSS 插件 `postcss-remove-wxss-incompatible.js` 自动移除对应的 CSS 规则:
1. **`!xxx` important 修饰符**(如 `!visible`):生成 `.!visible` 选择器WXSS 不支持反斜杠转义的 `!`
2. **带 `0.5` 的类名**(如 `w-0.5``h-0.5``gap-0.5`):类名中的 `.` 被转义为 `\.`,生成 `.w-0\.5` 这样的选择器WXSS 不兼容。应改用等效的整数尺寸或自定义值替代。
3. **`space-x-*` / `space-y-*`**(如 `space-y-4`):依赖 `:not()` + `~` 兄弟组合器 + CSS 变量 + 嵌套 `calc()`WXSS 全不支持。应改用 `flex flex-col gap-*` 实现等效间距。
4. **`divide-x-*` / `divide-y-*`**:同样依赖 `:not()` + `~`WXSS 不兼容。
## 替代方案速查
| 禁用类名 | 替代写法 |
|---------|---------|
| `space-y-4` | `flex flex-col gap-4` |
| `space-x-4` | `flex flex-row gap-4` |
| `!visible` | 直接写 `style={{ visibility: 'visible' }}` 或自定义类 |
| `w-0.5` | 用自定义值或 `w-1` 替代 |
## 微信小程序接口合规
### 剪切板接口(运营规范 5.15.4
1. **只允许用户主动触发复制**:所有 `Taro.setClipboardData` 调用必须绑定在明确的用户点击事件上,禁止在无用户操作时自动读写剪切板。
2. **禁止复制后强制中断流程**:复制成功后不得强制跳转、强制关注、强制添加客服等,不能要求用户通过其他方式才能完成当前业务流程。
3. **避免重复/失败 Toast 干扰体验**:微信小程序调用 `setClipboardData` 成功后系统会自带「内容已复制」提示。为避免重复提示及误报失败,统一封装 `utils/common.ts` 中的 `copyText` 函数:
- 小程序端weapp直接调用原生 `wx.setClipboardData`,不再额外显示自定义 `复制成功` / `复制失败` Toast
- 失败时仅通过 `console.error` 输出错误日志,便于排查
- 非小程序端保留原有成功/失败提示
### 项目内剪切板使用现状
已审查所有 `setClipboardData` 调用:
- 均通过按钮点击触发(复制咨询模板、复制密钥、复制链接、复制兑换码等)
-`getClipboardData`(自动读取剪切板)调用
- 无复制后强制跳转或中断业务流程的行为
- 首页「立即开通」等外链按钮点击后复制链接;相对路径入口(如开发者中心)改为 `Taro.navigateTo` 跳转,不再复制
当前实现基本符合 5.15.4 规范。
## AppSubscription 状态字段语义(踩坑必读)
判断"是否已支付"必须用 `payStatus`0=未支付 / 1=已支付)这个**支付状态**字段,**不能**用 `status`(订阅生命周期 active/pending/expired/cancelled
- `status`:订阅的生命周期状态,**不等于支付状态**。后端 `renewPay` 为了避免放弃支付导致服务中断,会故意保留原 `status='active'`、只把 `payStatus` 设为 0。
- `payStatus`本次订单的支付状态。支付成功mp-confirm / handleBalancePay后才置 1。
续费场景下这两个字段完全解耦:用 `status === 'active'` 判支付会**直接误判为已支付**,详见 `2026-07-16.md` 修复记录。

View File

@@ -0,0 +1,46 @@
/**
* PostCSS 插件:移除 WXSS 不兼容的 Tailwind CSS 规则
*
* 微信小程序 WXSS 不支持以下 CSS 特性:
* 1. 反斜杠转义的特殊字符(如 .\!visible—— !important 修饰符
* 2. :not() 伪类 + ~ 兄弟选择器组合(如 .space-y-4 > :not([hidden]) ~ :not([hidden])
* 3. CSS 自定义属性 / CSS 变量(如 --tw-space-y-reverse
* 4. 嵌套 calc()(如 calc(0.5rem * calc(1 - var(--tw-space-y-reverse)))
*
* 该插件移除以下类型的 Tailwind CSS 规则:
* - .\!xxx 类选择器important 修饰符)
* - .space-[x|y]-* 规则(间距布局,依赖 :not + ~ + CSS 变量 + 嵌套 calc
* - .divide-[x|y]-* 规则(分割线布局,同样依赖不兼容特性)
*/
/** @type {import('postcss').PluginCreator} */
module.exports = (options = {}) => {
return {
postcssPlugin: 'postcss-remove-wxss-incompatible',
Rule(rule) {
const selector = rule.selector
// 1) 移除 .\!xxx 类选择器Tailwind important 修饰符,反斜杠转义的 ! 不兼容 WXSS
const selectors = selector.split(',')
const hasEscapedImportant = selectors.some(s => /\.\\!/.test(s.trim()))
if (hasEscapedImportant) {
rule.remove()
return
}
// 2) 移除 .space-[x|y]-* 规则(依赖 :not + ~ 兄弟组合器WXSS 不兼容)
if (/\.space-[xy]-/.test(selector)) {
rule.remove()
return
}
// 3) 移除 .divide-[x|y]-* 规则(同上,依赖 :not + ~
if (/\.divide-[xy]-/.test(selector)) {
rule.remove()
return
}
}
}
}
module.exports.postcss = true

View File

@@ -1,8 +1,12 @@
const wxssIncompatiblePlugin = require('./postcss-remove-wxss-incompatible')
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {
remove: true // 禁用 autoprefixer避免添加浏览器前缀
},
},
plugins: [
require('tailwindcss'),
require('autoprefixer'),
require('postcss-nested'),
// 移除 WXSS 不兼容的 Tailwind CSS 规则
// 包括 .\!xxx反斜杠转义 !、space-x/y-*:not + ~ 组合器、divide-x/y-* 等
wxssIncompatiblePlugin()
]
}

View File

@@ -18,22 +18,23 @@ import type {
ApplyParam,
} from '@/types/developer'
// ==================== 项目相关 ====================
// ==================== 项目/应用相关 ====================
// 注意:后端使用 AppProduct 作为项目/应用的概念,路径为 /app/product
/**
* 获取我的项目列表
* 获取我的项目列表(我创建的应用)
*/
export async function pageMyProject(params?: ProjectParam) {
const res = await request.get<ApiResult<PageResult<Project>>>('/project/my/page', { params })
const res = await request.get<ApiResult<PageResult<Project>>>('/app/product/my/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取项目列表
* 获取项目列表(分页查询应用列表)
*/
export async function pageProject(params?: ProjectParam) {
const res = await request.get<ApiResult<PageResult<Project>>>('/project/page', { params })
const res = await request.get<ApiResult<PageResult<Project>>>('/app/product/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -42,7 +43,7 @@ export async function pageProject(params?: ProjectParam) {
* 获取项目详情
*/
export async function getProject(id: number) {
const res = await request.get<ApiResult<Project>>(`/project/${id}`)
const res = await request.get<ApiResult<Project>>(`/app/product/detail/${id}`)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -51,7 +52,7 @@ export async function getProject(id: number) {
* 创建项目
*/
export async function createProject(data: Partial<Project>) {
const res = await request.post<ApiResult<unknown>>('/project', data)
const res = await request.post<ApiResult<unknown>>('/app/product/create', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -60,7 +61,7 @@ export async function createProject(data: Partial<Project>) {
* 更新项目
*/
export async function updateProject(data: Partial<Project>) {
const res = await request.put<ApiResult<unknown>>(`/project/${data.id}`, data)
const res = await request.put<ApiResult<unknown>>('/app/product/update', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -69,16 +70,16 @@ export async function updateProject(data: Partial<Project>) {
* 删除项目
*/
export async function deleteProject(id: number) {
const res = await request.del<ApiResult<unknown>>(`/project/${id}`)
const res = await request.del<ApiResult<unknown>>(`/app/product/delete/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 获取项目成员列表
* 获取项目成员列表(使用应用用户服务)
*/
export async function listProjectMember(projectId: number) {
const res = await request.get<ApiResult<ProjectMember[]>>(`/project/${projectId}/members`)
const res = await request.get<ApiResult<ProjectMember[]>>(`/app/app-user/page`, { appId: projectId })
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
@@ -87,7 +88,7 @@ export async function listProjectMember(projectId: number) {
* 添加项目成员
*/
export async function addProjectMember(projectId: number, data: Partial<ProjectMember>) {
const res = await request.post<ApiResult<unknown>>(`/project/${projectId}/members`, data)
const res = await request.post<ApiResult<unknown>>(`/app/app-user`, { ...data, appId: projectId })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -96,18 +97,27 @@ export async function addProjectMember(projectId: number, data: Partial<ProjectM
* 移除项目成员
*/
export async function removeProjectMember(projectId: number, memberId: number) {
const res = await request.del<ApiResult<unknown>>(`/project/${projectId}/members/${memberId}`)
const res = await request.del<ApiResult<unknown>>(`/app/app-user/${memberId}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 应用相关 ====================
// ==================== 应用相关(别名,与项目共用)====================
/**
* 分页查询我的应用
* 分页查询我的应用(创建的应用)
*/
export async function pageMyApp(params?: AppParam) {
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/my/page', { params })
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/my/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 分页查询我参与的应用(通过邀请加入的应用)
*/
export async function pageJoinedApp(params?: AppParam) {
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/joined/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -116,7 +126,7 @@ export async function pageMyApp(params?: AppParam) {
* 获取应用列表
*/
export async function pageApp(params?: AppParam) {
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/page', { params })
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -125,7 +135,7 @@ export async function pageApp(params?: AppParam) {
* 获取应用详情
*/
export async function getApp(id: number) {
const res = await request.get<ApiResult<App>>(`/app/product/${id}`)
const res = await request.get<ApiResult<App>>(`/app/product/detail/${id}`)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -134,7 +144,7 @@ export async function getApp(id: number) {
* 创建应用
*/
export async function createApp(data: Partial<App>) {
const res = await request.post<ApiResult<unknown>>('/app/product', data)
const res = await request.post<ApiResult<unknown>>('/app/product/create', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -143,7 +153,7 @@ export async function createApp(data: Partial<App>) {
* 更新应用
*/
export async function updateApp(data: Partial<App>) {
const res = await request.put<ApiResult<unknown>>(`/app/product/${data.id}`, data)
const res = await request.put<ApiResult<unknown>>('/app/product/update', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -152,18 +162,19 @@ export async function updateApp(data: Partial<App>) {
* 删除应用
*/
export async function deleteApp(id: number) {
const res = await request.del<ApiResult<unknown>>(`/app/product/${id}`)
const res = await request.del<ApiResult<unknown>>(`/app/product/delete/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== API Key 相关 ====================
// 对应后端 AppCredentialController路径为 /app/app-credential
/**
* 获取 API Key 列表
*/
export async function pageApiKey(params?: ApiKeyParam) {
const res = await request.get<ApiResult<PageResult<ApiKey>>>('/app/app-credential/page', { params })
const res = await request.get<ApiResult<PageResult<ApiKey>>>('/app/app-credential/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -204,13 +215,23 @@ export async function deleteApiKey(id: number) {
return Promise.reject(new Error(res.message))
}
/**
* 重置 API Key Secret
*/
export async function resetApiKeySecret(id: number) {
const res = await request.post<ApiResult<unknown>>(`/app/app-credential/resetSecret/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 版本发布相关 ====================
// 对应后端 AppVersionController路径为 /app/app-version
/**
* 获取版本列表
*/
export async function pageVersion(params?: VersionParam) {
const res = await request.get<ApiResult<PageResult<Version>>>('/app/app-version/page', { params })
const res = await request.get<ApiResult<PageResult<Version>>>('/app/app-version/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -233,42 +254,80 @@ export async function createVersion(data: Partial<Version>) {
return Promise.reject(new Error(res.message))
}
// ==================== 开发者相关 ====================
/**
* 发布版本
*/
export async function publishVersion(id: number) {
const res = await request.post<ApiResult<unknown>>(`/app/app-version/publish/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 获取开发者信息
* 回滚版本
*/
export async function rollbackVersion(id: number) {
const res = await request.post<ApiResult<unknown>>(`/app/app-version/rollback/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 开发者相关 ====================
// 对应后端 GitAccountController路径为 /app/developer
/**
* 获取开发者信息Git账号绑定状态
*/
export async function getDeveloperInfo() {
const res = await request.get<ApiResult<Developer>>('/developer/info')
const res = await request.get<ApiResult<Developer>>('/app/developer/git-account')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 申请成为开发者
* 申请成为开发者绑定Git账号
*/
export async function applyDeveloper(data: DeveloperApplyParam) {
const res = await request.post<ApiResult<unknown>>('/developer/apply', data)
const res = await request.post<ApiResult<unknown>>('/app/developer/git-account', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 更新开发者信息
* 更新开发者信息更新Git账号
*/
export async function updateDeveloperInfo(data: Partial<Developer>) {
const res = await request.put<ApiResult<unknown>>('/developer/info', data)
const res = await request.put<ApiResult<unknown>>('/app/developer/git-account', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 获取Gitea服务器信息
*/
export async function getGiteaInfo() {
const res = await request.get<ApiResult<{ url: string; version: string; registrationEnabled: boolean }>>('/app/developer/gitea-info')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
// ==================== 消息通知相关 ====================
// 对应后端 AppNotificationController路径为 /app/notification
/**
* 获取通知列表
*/
export async function pageNotification(params?: NotificationParam) {
const res = await request.get<ApiResult<PageResult<Notification>>>('/notification/page', { params })
const res = await request.get<ApiResult<PageResult<Notification>>>('/app/notification/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取最近通知(铃铛下拉)
*/
export async function getRecentNotifications(type?: string, limit?: number) {
const res = await request.get<ApiResult<Notification[]>>('/app/notification/recent', { type, limit })
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -277,7 +336,7 @@ export async function pageNotification(params?: NotificationParam) {
* 获取未读通知数量
*/
export async function getUnreadCount() {
const res = await request.get<ApiResult<{ count: number }>>('/notification/unread-count')
const res = await request.get<ApiResult<{ count: number }>>('/app/notification/unread-count')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -286,7 +345,7 @@ export async function getUnreadCount() {
* 标记通知为已读
*/
export async function markAsRead(id: number) {
const res = await request.put<ApiResult<unknown>>(`/notification/${id}/read`)
const res = await request.put<ApiResult<unknown>>(`/app/notification/read/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -294,8 +353,8 @@ export async function markAsRead(id: number) {
/**
* 标记所有通知为已读
*/
export async function markAllAsRead() {
const res = await request.put<ApiResult<unknown>>('/notification/read-all')
export async function markAllAsRead(type?: string) {
const res = await request.put<ApiResult<unknown>>('/app/notification/read-all', { type })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -304,18 +363,28 @@ export async function markAllAsRead() {
* 删除通知
*/
export async function deleteNotification(id: number) {
const res = await request.del<ApiResult<unknown>>(`/notification/${id}`)
const res = await request.del<ApiResult<unknown>>(`/app/notification/${id}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 清空已读通知
*/
export async function clearReadNotifications(type?: string) {
const res = await request.del<ApiResult<unknown>>('/app/notification/clear-read', { type })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 权限审批相关 ====================
// 对应后端 AppPermissionRequestController路径为 /app/developer/permission-requests
/**
* 获取申请列表
*/
export async function pageApply(params?: ApplyParam) {
const res = await request.get<ApiResult<PageResult<Apply>>>('/apply/page', { params })
const res = await request.get<ApiResult<PageResult<Apply>>>('/app/developer/permission-requests/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -324,7 +393,25 @@ export async function pageApply(params?: ApplyParam) {
* 获取我的申请列表
*/
export async function pageMyApply(params?: ApplyParam) {
const res = await request.get<ApiResult<PageResult<Apply>>>('/apply/my/page', { params })
const res = await request.get<ApiResult<PageResult<Apply>>>('/app/developer/permission-requests', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取权限申请统计
*/
export async function getApplyStats() {
const res = await request.get<ApiResult<{ [key: string]: number }>>('/app/developer/permission-requests/stats')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
/**
* 获取可申请的仓库列表
*/
export async function getAvailableRepos() {
const res = await request.get<ApiResult<Array<{ name: string; fullName: string }>>>('/app/developer/permission-requests/available-repos')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -332,17 +419,26 @@ export async function pageMyApply(params?: ApplyParam) {
/**
* 创建申请
*/
export async function createApply(data: Partial<Apply>) {
const res = await request.post<ApiResult<unknown>>('/apply', data)
export async function createApply(data: { repo: string; reason: string; gitUsername?: string }) {
const res = await request.post<ApiResult<unknown>>('/app/developer/permission-requests', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 审批申请
* 审批申请-通过
*/
export async function reviewApply(id: number, status: 'approved' | 'rejected', remark?: string) {
const res = await request.put<ApiResult<unknown>>(`/apply/${id}/review`, { status, remark })
export async function approveApply(id: number, note?: string) {
const res = await request.put<ApiResult<unknown>>(`/app/developer/permission-requests/${id}/approve`, { note })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
/**
* 审批申请-拒绝
*/
export async function rejectApply(id: number, reason: string) {
const res = await request.put<ApiResult<unknown>>(`/app/developer/permission-requests/${id}/reject`, { reason })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}

View File

@@ -2,13 +2,13 @@ import request from '@/utils/request'
import type { ApiResult, PageResult } from '@/api'
import type { Enterprise, EnterpriseMember, EnterpriseMemberParam, Order, Bill, BillParam, App, AppParam } from '@/types/developer'
// ==================== 企业相关 ====================
// ==================== 企业/租户相关 ====================
/**
* 获取企业信息
*/
export async function getEnterpriseInfo() {
const res = await request.get<ApiResult<Enterprise>>('/enterprise/info')
const res = await request.get<ApiResult<Enterprise>>('/system/tenant/info')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -17,18 +17,18 @@ export async function getEnterpriseInfo() {
* 更新企业信息
*/
export async function updateEnterpriseInfo(data: Partial<Enterprise>) {
const res = await request.put<ApiResult<unknown>>('/enterprise/info', data)
const res = await request.put<ApiResult<unknown>>('/system/tenant', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 企业成员相关 ====================
// ==================== 企业成员/用户相关 ====================
/**
* 获取企业成员列表
*/
export async function pageEnterpriseMember(params?: EnterpriseMemberParam) {
const res = await request.get<ApiResult<PageResult<EnterpriseMember>>>('/enterprise/member/page', { params })
const res = await request.get<ApiResult<PageResult<EnterpriseMember>>>('/system/user/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -37,7 +37,7 @@ export async function pageEnterpriseMember(params?: EnterpriseMemberParam) {
* 获取企业成员列表(不分页)
*/
export async function listEnterpriseMember(params?: EnterpriseMemberParam) {
const res = await request.get<ApiResult<EnterpriseMember[]>>('/enterprise/member', params)
const res = await request.get<ApiResult<EnterpriseMember[]>>('/system/user', params)
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
@@ -46,7 +46,7 @@ export async function listEnterpriseMember(params?: EnterpriseMemberParam) {
* 邀请企业成员
*/
export async function inviteEnterpriseMember(enterpriseId: number, data: Partial<EnterpriseMember>) {
const res = await request.post<ApiResult<unknown>>(`/enterprise/member/${enterpriseId}/invite`, data)
const res = await request.post<ApiResult<unknown>>(`/app/developer/invite/${enterpriseId}/invite`, data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -55,7 +55,7 @@ export async function inviteEnterpriseMember(enterpriseId: number, data: Partial
* 更新企业成员
*/
export async function updateEnterpriseMember(data: Partial<EnterpriseMember>) {
const res = await request.put<ApiResult<unknown>>(`/enterprise/member/${data.id}`, data)
const res = await request.put<ApiResult<unknown>>(`/system/user`, data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -64,7 +64,7 @@ export async function updateEnterpriseMember(data: Partial<EnterpriseMember>) {
* 移除企业成员
*/
export async function removeEnterpriseMember(memberId: number) {
const res = await request.del<ApiResult<unknown>>(`/enterprise/member/${memberId}`)
const res = await request.del<ApiResult<unknown>>(`/system/user/${memberId}`)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
@@ -75,7 +75,7 @@ export async function removeEnterpriseMember(memberId: number) {
* 获取订单列表
*/
export async function pageOrder(params?: { page?: number; limit?: number; status?: number }) {
const res = await request.get<ApiResult<PageResult<Order>>>('/enterprise/order/page', { params })
const res = await request.get<ApiResult<PageResult<Order>>>('/system/order/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -84,18 +84,27 @@ export async function pageOrder(params?: { page?: number; limit?: number; status
* 获取订单详情
*/
export async function getOrder(id: number) {
const res = await request.get<ApiResult<Order>>(`/enterprise/order/${id}`)
const res = await request.get<ApiResult<Order>>(`/system/order/${id}`)
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
// ==================== 账单相关 ====================
/**
* 创建订单
*/
export async function createOrder(data: any) {
const res = await request.post<ApiResult<unknown>>('/system/order', data)
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}
// ==================== 充值/账单相关 ====================
/**
* 获取账单列表
*/
export async function pageBill(params?: BillParam) {
const res = await request.get<ApiResult<PageResult<Bill>>>('/enterprise/bill/page', { params })
const res = await request.get<ApiResult<PageResult<Bill>>>('/sys/recharge-order/page', params)
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -104,7 +113,7 @@ export async function pageBill(params?: BillParam) {
* 获取账单概览
*/
export async function getBillOverview() {
const res = await request.get<ApiResult<{ balance: number; totalConsume: number; totalRecharge: number }>>('/enterprise/bill/overview')
const res = await request.get<ApiResult<{ balance: number; totalConsume: number; totalRecharge: number }>>('/sys/user-balance-log/overview')
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -115,7 +124,7 @@ export async function getBillOverview() {
* 获取企业应用列表
*/
export async function pageEnterpriseApp(params?: AppParam) {
const res = await request.get<ApiResult<PageResult<App>>>('/enterprise/app/page', { params })
const res = await request.get<ApiResult<PageResult<App>>>('/app/product/page', { params })
if (res.code === 0) return res.data
return Promise.reject(new Error(res.message))
}
@@ -124,7 +133,7 @@ export async function pageEnterpriseApp(params?: AppParam) {
* 获取企业应用详情
*/
export async function getEnterpriseApp(id: number) {
const res = await request.get<ApiResult<App>>(`/enterprise/app/${id}`)
const res = await request.get<ApiResult<App>>(`/app/product/${id}`)
if (res.code === 0 && res.data) return res.data
return Promise.reject(new Error(res.message))
}
@@ -133,7 +142,7 @@ export async function getEnterpriseApp(id: number) {
* 购买应用
*/
export async function purchaseApp(productId: number) {
const res = await request.post<ApiResult<unknown>>('/enterprise/app/purchase', { productId })
const res = await request.post<ApiResult<unknown>>('/api/system/order/createOrder', { productId })
if (res.code === 0) return res.message
return Promise.reject(new Error(res.message))
}

View File

@@ -108,10 +108,15 @@ export interface InviteRecordParam {
/**
* 生成小程序码
*
* 注意envVersion 参数会作为查询参数传给后端,
* 后端调用 wxacode.getUnlimited 时需使用该值。
*/
export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
try {
const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene;
// 默认使用体验版(开发/测试阶段),提审上线前改为 'release'
const envVersion = data.envVersion || 'trial'
const url = `/wx-login/getOrderQRCodeUnlimited/${data.scene}?envVersion=${envVersion}`;
// 由于接口直接返回图片buffer我们直接构建完整的URL
return `${BaseUrl}${url}`;
} catch (error: any) {

View File

@@ -90,6 +90,8 @@ export async function loginByOpenId(data: WxLoginParam): Promise<{
/**
* 获取微信 OpenId仅获取不登录
* 注意:后端 /wx-login/getOpenId 返回的是 LoginResult { access_token, user }
* 真正的 openid 在 user.openid 里
*/
export async function getOpenId(code: string): Promise<{
success: boolean;
@@ -97,22 +99,24 @@ export async function getOpenId(code: string): Promise<{
unionid?: string;
session_key?: string;
message?: string;
access_token?: string;
user?: WxLoginUserInfo;
}> {
const res = await request.post<ApiResult<{
openid: string;
unionid?: string;
session_key?: string;
}>>(
const res = await request.post<ApiResult<WxLoginResult>>(
SERVER_API_URL + '/wx-login/getOpenId',
{ code }
);
console.log('[WxLogin] getOpenId 响应:', res);
if ((res.code === 0 || res.code === 200) && res.data) {
return {
success: true,
openid: res.data.openid,
unionid: res.data.unionid,
session_key: res.data.session_key
openid: res.data.user?.openid,
unionid: res.data.user?.unionid,
session_key: res.data.user?.session_key,
access_token: res.data.access_token,
user: res.data.user
};
}

View File

@@ -128,3 +128,24 @@ export async function submit(data: UserVerify) {
}
return Promise.reject(new Error(res.message));
}
/**
* 阿里云实人认证 - 身份证二要素核验
* 验证姓名和身份证号是否一致
*/
export async function verifyIdCard(realName: string, idCard: string) {
const res = await request.post<ApiResult<{
success: boolean;
isMatch: boolean;
bizCode: string;
message: string;
}>>(
SERVER_API_URL + '/id-verification/verify-id-card',
null,
{ params: { realName, idCard } }
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -20,7 +20,8 @@ export default {
'qr-confirm/index',
'invite/index',
'unified-qr/index',
'webview/index'
'webview/index',
'pay/index'
]
},
{

View File

@@ -233,7 +233,7 @@ const QRScanModal: React.FC<QRScanModalProps> = ({
)}
{status === 'error' && (
<View className="space-y-2">
<View className="flex flex-col gap-2">
<Button
type="primary"
size="large"

View File

@@ -126,7 +126,7 @@ const InviteStatsPage: React.FC = () => {
// 渲染统计概览
const renderStatsOverview = () => (
<View className="px-4 space-y-4">
<View className="px-4 flex flex-col gap-4">
{/* 核心数据卡片 */}
<Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4">
@@ -182,7 +182,7 @@ const InviteStatsPage: React.FC = () => {
<Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4">
<Text className="text-lg font-semibold text-gray-800 mb-4"></Text>
<View className="space-y-3">
<View className="flex flex-col gap-3">
{inviteStats.sourceStats.map((source, index) => (
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<View className="flex items-center">
@@ -208,7 +208,7 @@ const InviteStatsPage: React.FC = () => {
const renderInviteRecords = () => (
<View className="px-4">
{inviteRecords.length > 0 ? (
<View className="space-y-3">
<View className="flex flex-col gap-3">
{inviteRecords.map((record, index) => (
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
<View className="p-4">
@@ -253,7 +253,7 @@ const InviteStatsPage: React.FC = () => {
</View>
{ranking.length > 0 ? (
<View className="space-y-3">
<View className="flex flex-col gap-3">
{ranking.map((item, index) => (
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
<View className="p-4 flex items-center">

View File

@@ -377,7 +377,7 @@ const DealerQrcode: React.FC = () => {
{/* 推广说明 */}
<View className="bg-white rounded-2xl p-4 mt-6 hidden">
<Text className="font-semibold text-gray-800 mb-3">广</Text>
<View className="space-y-2">
<View className="flex flex-col gap-2">
<View className="flex items-start">
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
@@ -408,7 +408,7 @@ const DealerQrcode: React.FC = () => {
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
{/* </View>*/}
{/* ) : inviteStats ? (*/}
{/* <View className="space-y-4">*/}
{/* <View className="flex flex-col gap-4">*/}
{/* <View className="grid grid-cols-2 gap-4">*/}
{/* <View className="text-center">*/}
{/* <Text className="text-2xl font-bold text-blue-500">*/}
@@ -443,7 +443,7 @@ const DealerQrcode: React.FC = () => {
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
{/* <View className="mt-4">*/}
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
{/* <View className="space-y-2">*/}
{/* <View className="flex flex-col gap-2">*/}
{/* {inviteStats.sourceStats.map((source, index) => (*/}
{/* <View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">*/}
{/* <View className="flex items-center">*/}

View File

@@ -1,5 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '开发者中心',
navigationStyle: 'custom',
usingComponents: {},
})

View File

@@ -3,7 +3,7 @@ import { View, Text, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { Button, Empty } from '@nutui/nutui-react-taro'
import { useThemeStyles } from '@/hooks/useTheme'
import { pageMyApp } from '@/api/developer'
import { pageMyApp, pageJoinedApp } from '@/api/developer'
import type { App } from '@/types/developer'
import { APP_TYPE_NAME } from '@/types/developer'
import './index.scss'
@@ -18,19 +18,45 @@ const DeveloperIndex: React.FC = () => {
totalCalls: 0,
})
// 加载数据
// 加载数据 - 同时查询创建的应用和参与的应用
const loadData = async () => {
try {
setLoading(true)
const result = await pageMyApp({ current: 1, size: 10 })
if (result) {
setApps(result.list || [])
// 同时调用两个接口:创建的应用 + 参与的应用(通过邀请加入)
const [myAppsResult, joinedAppsResult] = await Promise.all([
pageMyApp({ current: 1, size: 10 }),
pageJoinedApp({ current: 1, size: 10 }),
])
// 合并两个列表,去重(根据 productId
const myApps = myAppsResult?.records || []
const joinedApps = joinedAppsResult?.records || []
// 使用 Map 去重,优先保留创建的应用(排在前面)
const appMap = new Map<number, App>()
// 先加入创建的应用
myApps.forEach((app) => {
if (app.productId) {
appMap.set(app.productId, { ...app, isOwner: true })
}
})
// 再加入参与的应用(如果已存在则跳过)
joinedApps.forEach((app) => {
if (app.productId && !appMap.has(app.productId)) {
appMap.set(app.productId, { ...app, isOwner: false })
}
})
const mergedApps = Array.from(appMap.values())
setApps(mergedApps)
setStats({
totalApps: result.count || 0,
runningApps: result.list?.filter((a) => a.status === 1).length || 0,
totalApps: mergedApps.length,
runningApps: mergedApps.filter((a) => a.status === 1).length,
totalCalls: 0,
})
}
} catch (error) {
console.error('加载数据失败', error)
Taro.showToast({ title: '加载失败', icon: 'none' })

View File

@@ -1,2 +1,3 @@
// 复用 developer/app/api-keys/index.scss 的样式
@import '../../developer/app/api-keys/index';
// 注意:使用 @use 替代 @import 是 Sass 的新推荐方式
@use '../../app/api-keys/index' as api-keys;

View File

@@ -1,5 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '企业控制台',
navigationStyle: 'custom',
usingComponents: {},
})

View File

@@ -3,7 +3,6 @@ import { View, Text } from '@tarojs/components'
import { useMemo, useState } from 'react'
import { Button } from '@nutui/nutui-react-taro'
import { useConfig } from '@/hooks/useConfig'
import { copyText } from '@/utils/common'
import './index.scss'
function Home() {
@@ -70,28 +69,23 @@ function Home() {
const openMaybeLink = (url?: string) => {
if (!url) return
// 小程序内无法直接打开外链,这里统一“复制链接”降低认知成本
const abs = toAbsoluteMaybe(url)
// 相对路径在小程序内直接跳转
if (url.startsWith('/')) {
Taro.navigateTo({ url })
return
}
// 小程序内无法直接打开外链,提示用户手动复制到浏览器
if (/^https?:\/\//i.test(abs)) {
copyText(abs)
Taro.showModal({
title: '提示',
content: '该链接需要在浏览器中打开,请点击确认后手动复制链接访问。',
showCancel: false,
confirmText: '知道了'
})
return
}
Taro.showToast({ title: '请在 PC 端访问该入口', icon: 'none', duration: 1600 })
copyText(abs)
}
const copyConsultTemplate = () => {
const tel = config?.tel ? `\n联系电话${config.tel}` : ''
const text =
'【需求咨询】\n' +
'1) 想开通哪些产品:企业官网 / 电商 / 小程序 / 其他\n' +
'2) 是否需要模板/插件市场:是 / 否\n' +
'3) 是否需要“支付即开通”:是 / 否\n' +
'4) 交付方式SaaS / 私有化 / 混合\n' +
'5) 合规/部署要求:\n' +
'6) 期望上线时间:\n' +
tel
copyText(text)
}
const callPhone = () => {
@@ -440,7 +434,6 @@ function Home() {
block
onClick={() => {
Taro.showToast({ title: '该入口可对接到模板/插件市场', icon: 'none', duration: 1600 })
openMaybeLink('/market')
}}
>
/
@@ -453,7 +446,7 @@ function Home() {
<View className="section__head">
<Text className="section__title"></Text>
<Text className="section__desc">
SaaS/
SaaS/
</Text>
</View>
@@ -489,9 +482,6 @@ function Home() {
</View>
<View className="contactActions">
<Button type="default" block onClick={copyConsultTemplate}>
</Button>
<Button type="primary" block onClick={callPhone}>
</Button>

View File

@@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '确认支付',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
}

View File

@@ -0,0 +1,379 @@
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import { Button, Cell, Price, Divider } from '@nutui/nutui-react-taro'
import { Check, Close, Tips, Clock } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { request } from '@/utils/request'
import { getOpenId, loginByOpenId } from '@/api/passport/wx-login'
import { saveStorageByLoginUser } from '@/utils/server'
import { TenantId } from '@/config/app'
/**
* 小程序支付页面
* 接收参数 subscriptionNo完成 JSAPI 支付
*/
interface SubscriptionDetail {
id: number
subscriptionNo: string
productId: number
productName: string
productLogo?: string
productIcon?: string
status: string
priceType: string
payPrice: number
subscriptionPeriod?: string
}
interface JsapiPayParams {
timeStamp: string
nonceStr: string
package: string
signType: string
paySign: string
outTradeNo: string
}
const PayPage: React.FC = () => {
const [loading, setLoading] = useState(true)
const [paying, setPaying] = useState(false)
const [detail, setDetail] = useState<SubscriptionDetail | null>(null)
const [errorMsg, setErrorMsg] = useState('')
const [paid, setPaid] = useState(false)
// 从页面参数中获取 subscriptionNo
// 小程序码 scene 会作为 query.scene 传入,需解码
const getSubscriptionNo = useCallback(() => {
const instance = Taro.getCurrentInstance()
const params = instance?.router?.params || {}
console.log('[PayPage] ===== 参数调试开始 =====')
console.log('[PayPage] 原始 params:', JSON.stringify(params))
console.log('[PayPage] params.scene:', params.scene)
console.log('[PayPage] params.subscriptionNo:', params.subscriptionNo)
// 优先读取 scene小程序码参数再读取 subscriptionNo普通链接参数
const scene = params.scene ? decodeURIComponent(params.scene) : ''
const subscriptionNo = scene || params.subscriptionNo || ''
console.log('[PayPage] decodeURIComponent(scene):', scene)
console.log('[PayPage] 最终 subscriptionNo:', subscriptionNo)
console.log('[PayPage] ===== 参数调试结束 =====')
return subscriptionNo
}, [])
// 获取订阅详情
const fetchDetail = useCallback(async () => {
const subscriptionNo = getSubscriptionNo()
if (!subscriptionNo) {
console.error('[PayPage] subscriptionNo 为空,无法请求')
setErrorMsg('缺少订单参数')
setLoading(false)
return
}
console.log('[PayPage] ===== 请求调试开始 =====')
console.log('[PayPage] 请求 URL:', `/app/subscription/detail-by-no/${subscriptionNo}`)
console.log('[PayPage] 当前 token:', Taro.getStorageSync('access_token') || '(无token)')
try {
// request 默认 returnRaw=false拦截器已拆包返回的就是 data 部分(订阅对象)
const data: any = await request({
url: `/app/subscription/detail-by-no/${subscriptionNo}`,
method: 'GET',
returnRaw: true // 先用 returnRaw=true 拿到完整响应,方便调试
})
console.log('[PayPage] 接口完整返回returnRaw=true:', JSON.stringify(data))
console.log('[PayPage] data.code:', data?.code)
console.log('[PayPage] data.message:', data?.message)
console.log('[PayPage] data.data:', data?.data)
// 判断业务状态码
if (data?.code === 0 || data?.code === 200) {
const subscription = data.data || data
console.log('[PayPage] 订阅对象:', JSON.stringify(subscription))
console.log('[PayPage] 订阅状态:', subscription?.status)
if (subscription?.status === 'active') {
setPaid(true)
setDetail(subscription)
} else {
setDetail(subscription)
}
} else {
console.error('[PayPage] 业务错误 code:', data?.code, 'message:', data?.message)
setErrorMsg(data?.message || `业务错误(code=${data?.code})`)
}
} catch (err: any) {
console.error('[PayPage] ===== 请求异常 =====')
console.error('[PayPage] err.name:', err?.name)
console.error('[PayPage] err.type:', err?.type)
console.error('[PayPage] err.code:', err?.code)
console.error('[PayPage] err.message:', err?.message)
console.error('[PayPage] err.data:', err?.data)
console.error('[PayPage] err 完整:', JSON.stringify(err))
setErrorMsg(err?.message || '网络异常,请重试')
} finally {
setLoading(false)
console.log('[PayPage] ===== 请求调试结束 =====')
}
}, [getSubscriptionNo])
useEffect(() => {
fetchDetail()
}, [fetchDetail])
// 执行支付
const handlePay = async () => {
if (!detail) return
setPaying(true)
try {
// 1. 获取微信登录凭证code
const loginRes = await Taro.login()
if (!loginRes.code) {
throw new Error('获取登录凭证失败')
}
console.log('[PayPage] Taro.login code:', loginRes.code)
// 2. 用 code 换取 openid关键步骤之前遗漏了
const openIdRes = await getOpenId(loginRes.code)
console.log('[PayPage] getOpenId 结果:', openIdRes)
if (!openIdRes.success || !openIdRes.openid) {
throw new Error(openIdRes.message || '获取 openid 失败,请重试')
}
const openid = openIdRes.openid
// 缓存 openid后续支付确认等接口可用
Taro.setStorageSync('openid', openid)
console.log('[PayPage] 获取到 openid:', openid)
// 3. 尝试自动登录(获取 tokenmp-prepay 接口需要登录态)
// 如果用户已在小程序注册过loginByOpenId 会返回 token
// 如果用户未注册,不影响 openid 已获取,但 mp-prepay 可能需要特殊处理
try {
const loginResult = await loginByOpenId({
code: loginRes.code,
tenantId: TenantId
})
console.log('[PayPage] loginByOpenId 结果:', loginResult)
if (loginResult.success && loginResult.data) {
// 登录成功,保存 token
saveStorageByLoginUser(loginResult.data.access_token || '', loginResult.data.user)
console.log('[PayPage] 自动登录成功,已保存 token')
} else {
console.warn('[PayPage] 自动登录失败:', loginResult.message)
// 未注册用户openid 已经有了,但 mp-prepay 需要 userId
// 提示用户先注册/登录
if (loginResult.message?.includes('未注册') || loginResult.message?.includes('不存在')) {
Taro.showModal({
title: '需要先登录',
content: '支付前需要先注册账号,是否前往登录?',
confirmText: '去登录',
cancelText: '取消'
}).then(modalRes => {
if (modalRes.confirm) {
Taro.navigateTo({ url: '/passport/login' })
}
})
return // 不继续支付流程
}
}
} catch (loginErr) {
console.warn('[PayPage] 自动登录异常:', loginErr)
// 登录异常但不阻断支付openid 已拿到,尝试继续
}
// 4. 请求后端创建 JSAPI 预支付订单
const prepayRes: any = await request({
url: `/app/subscription/mp-prepay/${detail.id}`,
method: 'POST',
data: { openid },
returnRaw: true
})
console.log('[PayPage] mp-prepay 完整返回:', JSON.stringify(prepayRes))
if (prepayRes?.code !== 0 && prepayRes?.code !== 200) {
throw new Error(prepayRes?.message || '创建支付订单失败')
}
const payParams: JsapiPayParams = prepayRes.data || prepayRes
if (!payParams.timeStamp || !payParams.package || !payParams.paySign) {
throw new Error('支付参数不完整')
}
// 5. 调用微信支付
await Taro.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: (payParams.signType || 'RSA') as any,
paySign: payParams.paySign
})
// 6. 支付成功后通知后端确认
try {
await request({
url: `/app/subscription/mp-confirm/${detail.subscriptionNo}`,
method: 'POST',
data: {}
})
} catch (confirmErr) {
console.warn('支付确认通知失败,等待轮询:', confirmErr)
}
// 7. 显示成功状态
setPaid(true)
Taro.showToast({ title: '支付成功', icon: 'success' })
// 8. 延迟自动跳转到已购产品页面
setTimeout(() => {
Taro.navigateTo({ url: '/user/apps/index' }).catch(() => {
// 如果 navigateTo 失败(比如在 tab 页),回退到用户页
Taro.switchTab({ url: '/pages/user/user' })
})
}, 1500)
} catch (err: any) {
console.error('[PayPage] 支付失败:', err)
console.error('[PayPage] err.name:', err?.name, 'err.type:', err?.type)
console.error('[PayPage] err.code:', err?.code, 'err.message:', err?.message)
if (err?.errMsg?.includes('cancel')) {
Taro.showToast({ title: '支付已取消', icon: 'none' })
} else {
const msg = err?.message || err?.errMsg || '支付失败,请重试'
Taro.showToast({ title: msg, icon: 'error', duration: 2000 })
}
} finally {
setPaying(false)
}
}
// 返回
const handleBack = () => {
Taro.navigateBack({ delta: 1 }).catch(() => {
Taro.switchTab({ url: '/pages/user/user' })
})
}
// 格式化价格描述
const getPriceDesc = () => {
if (!detail) return ''
if (detail.priceType === 'one_time') return '永久买断'
if (detail.priceType === 'subscription') {
return detail.subscriptionPeriod === 'year' ? '按年订阅' : '按月订阅'
}
return ''
}
// --- 加载中 ---
if (loading) {
return (
<View className="pay-page min-h-screen bg-gray-50 flex items-center justify-center">
<View className="text-center">
<Clock className="text-blue-500 mb-4" size="48" />
<Text className="block text-gray-500">...</Text>
</View>
</View>
)
}
// --- 错误 ---
if (errorMsg) {
return (
<View className="pay-page min-h-screen bg-gray-50 flex items-center justify-center">
<View className="text-center p-8">
<Close className="text-red-500 mb-4" size="48" />
<Text className="block text-gray-800 text-lg mb-2"></Text>
<Text className="block text-gray-500 mb-6">{errorMsg}</Text>
<Button type="primary" onClick={handleBack}></Button>
</View>
</View>
)
}
// --- 已支付 ---
if (paid) {
return (
<View className="pay-page min-h-screen bg-gray-50 flex items-center justify-center">
<View className="text-center p-8">
<View className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Check className="text-green-500" size="40" />
</View>
<Text className="block text-green-600 text-xl font-bold mb-2"></Text>
<Text className="block text-gray-500 mb-2">{detail?.productName || '应用订阅'}</Text>
{detail?.payPrice ? (
<Text className="block text-gray-400 mb-6">
¥{Number(detail.payPrice).toFixed(2)}
</Text>
) : null}
<Button type="primary" onClick={handleBack}></Button>
</View>
</View>
)
}
// --- 支付确认 ---
return (
<View className="pay-page min-h-screen bg-gray-50">
<View className="p-4">
{/* 订单信息 */}
<View className="bg-white rounded-lg shadow-sm p-4 mb-4">
<Text className="block text-lg font-bold text-gray-800 mb-3"></Text>
<Cell title="商品名称" description={detail?.productName || '-'} />
<Cell title="价格类型" description={getPriceDesc()} />
<View className="flex items-center justify-between px-4 py-3">
<Text className="text-gray-600"></Text>
<Price
price={detail?.payPrice}
size="large"
thousands
/>
</View>
<Divider />
<View className="flex items-start px-4 py-2">
<Tips className="text-orange-500 mr-2 mt-1" size="16" />
<Text className="text-sm text-gray-500">
使
</Text>
</View>
</View>
{/* 支付按钮 */}
<View className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
<View className="flex gap-3">
<Button
block
onClick={handleBack}
className="flex-1"
>
</Button>
<Button
type="primary"
block
loading={paying}
onClick={handlePay}
className="flex-1"
>
{paying ? '支付中...' : '立即支付'}
</Button>
</View>
</View>
</View>
{/* 底部安全距离占位 */}
<View style={{ height: '100px' }} />
</View>
)
}
export default PayPage

View File

@@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '确认支付',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
}

400
src/passport/pay/index.tsx Normal file
View File

@@ -0,0 +1,400 @@
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import { Button, Cell, Price, Divider } from '@nutui/nutui-react-taro'
import { Check, Close, Tips, Clock } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { request } from '@/utils/request'
import { getOpenId, loginByOpenId } from '@/api/passport/wx-login'
import { saveStorageByLoginUser } from '@/utils/server'
import { TenantId } from '@/config/app'
/**
* 小程序支付页面
* 接收参数 subscriptionNo完成 JSAPI 支付
*
* 重要:判断订单是否已支付,必须用 payStatus0=未支付 1=已支付),
* 不能用 status订阅生命周期active/pending/expired/cancelled
* 续费场景下后端 renewPay 会保留原 status=active、仅设 payStatus=0
* 若用 status 判支付会直接误判为"已支付"。
*/
interface SubscriptionDetail {
id: number
subscriptionNo: string
productId: number
productName: string
productLogo?: string
productIcon?: string
status: string
// 支付状态: 0-未支付 1-已支付
payStatus?: number
payTime?: string
transactionId?: string
priceType: string
payPrice: number
subscriptionPeriod?: string
}
interface JsapiPayParams {
timeStamp: string
nonceStr: string
package: string
signType: string
paySign: string
outTradeNo: string
}
const PayPage: React.FC = () => {
const [loading, setLoading] = useState(true)
const [paying, setPaying] = useState(false)
const [detail, setDetail] = useState<SubscriptionDetail | null>(null)
const [errorMsg, setErrorMsg] = useState('')
const [paid, setPaid] = useState(false)
// 从页面参数中获取 subscriptionNo
// 小程序码 scene 会作为 query.scene 传入,需解码
const getSubscriptionNo = useCallback(() => {
const instance = Taro.getCurrentInstance()
const params = instance?.router?.params || {}
console.log('[PayPage] ===== 参数调试开始 =====')
console.log('[PayPage] 原始 params:', JSON.stringify(params))
console.log('[PayPage] params.scene:', params.scene)
console.log('[PayPage] params.subscriptionNo:', params.subscriptionNo)
// 优先读取 scene小程序码参数再读取 subscriptionNo普通链接参数
const scene = params.scene ? decodeURIComponent(params.scene) : ''
const subscriptionNo = scene || params.subscriptionNo || ''
console.log('[PayPage] decodeURIComponent(scene):', scene)
console.log('[PayPage] 最终 subscriptionNo:', subscriptionNo)
console.log('[PayPage] ===== 参数调试结束 =====')
return subscriptionNo
}, [])
// 获取订阅详情
const fetchDetail = useCallback(async () => {
const subscriptionNo = getSubscriptionNo()
if (!subscriptionNo) {
console.error('[PayPage] subscriptionNo 为空,无法请求')
setErrorMsg('缺少订单参数')
setLoading(false)
return
}
console.log('[PayPage] ===== 请求调试开始 =====')
console.log('[PayPage] 请求 URL:', `/app/subscription/detail-by-no/${subscriptionNo}`)
console.log('[PayPage] 当前 token:', Taro.getStorageSync('access_token') || '(无token)')
try {
// request 默认 returnRaw=false拦截器已拆包返回的就是 data 部分(订阅对象)
const data: any = await request({
url: `/app/subscription/detail-by-no/${subscriptionNo}`,
method: 'GET',
returnRaw: true // 先用 returnRaw=true 拿到完整响应,方便调试
})
console.log('[PayPage] 接口完整返回returnRaw=true:', JSON.stringify(data))
console.log('[PayPage] data.code:', data?.code)
console.log('[PayPage] data.message:', data?.message)
console.log('[PayPage] data.data:', data?.data)
// 判断业务状态码
if (data?.code === 0 || data?.code === 200) {
const subscription = data.data || data
console.log('[PayPage] 订阅对象:', JSON.stringify(subscription))
console.log('[PayPage] 订阅状态 status:', subscription?.status)
console.log('[PayPage] 支付状态 payStatus:', subscription?.payStatus)
console.log('[PayPage] 支付时间 payTime:', subscription?.payTime)
// 关键修复:判断是否已支付,必须用 payStatus 而非 status
// 续费场景下后端 renewPay 会保留原 status=active、仅设 payStatus=0
const isPaid = subscription?.payStatus === 1
const isCancelled = subscription?.status === 'cancelled'
const isExpired = subscription?.status === 'expired'
if (isCancelled) {
setErrorMsg('订单已取消,无法继续支付')
} else if (isExpired) {
setErrorMsg('订单已过期,请重新下单')
} else if (isPaid) {
setPaid(true)
setDetail(subscription)
} else {
setDetail(subscription)
}
} else {
console.error('[PayPage] 业务错误 code:', data?.code, 'message:', data?.message)
setErrorMsg(data?.message || `业务错误(code=${data?.code})`)
}
} catch (err: any) {
console.error('[PayPage] ===== 请求异常 =====')
console.error('[PayPage] err.name:', err?.name)
console.error('[PayPage] err.type:', err?.type)
console.error('[PayPage] err.code:', err?.code)
console.error('[PayPage] err.message:', err?.message)
console.error('[PayPage] err.data:', err?.data)
console.error('[PayPage] err 完整:', JSON.stringify(err))
setErrorMsg(err?.message || '网络异常,请重试')
} finally {
setLoading(false)
console.log('[PayPage] ===== 请求调试结束 =====')
}
}, [getSubscriptionNo])
useEffect(() => {
fetchDetail()
}, [fetchDetail])
// 执行支付
const handlePay = async () => {
if (!detail) return
setPaying(true)
try {
// 1. 获取微信登录凭证code
const loginRes = await Taro.login()
if (!loginRes.code) {
throw new Error('获取登录凭证失败')
}
console.log('[PayPage] Taro.login code:', loginRes.code)
// 2. 用 code 换取 openid关键步骤之前遗漏了
const openIdRes = await getOpenId(loginRes.code)
console.log('[PayPage] getOpenId 结果:', openIdRes)
if (!openIdRes.success || !openIdRes.openid) {
throw new Error(openIdRes.message || '获取 openid 失败,请重试')
}
const openid = openIdRes.openid
// 缓存 openid后续支付确认等接口可用
Taro.setStorageSync('openid', openid)
console.log('[PayPage] 获取到 openid:', openid)
// 3. 尝试自动登录(获取 tokenmp-prepay 接口需要登录态)
// 如果用户已在小程序注册过loginByOpenId 会返回 token
// 如果用户未注册,不影响 openid 已获取,但 mp-prepay 可能需要特殊处理
try {
const loginResult = await loginByOpenId({
code: loginRes.code,
tenantId: TenantId
})
console.log('[PayPage] loginByOpenId 结果:', loginResult)
if (loginResult.success && loginResult.data) {
// 登录成功,保存 token
saveStorageByLoginUser(loginResult.data.access_token || '', loginResult.data.user)
console.log('[PayPage] 自动登录成功,已保存 token')
} else {
console.warn('[PayPage] 自动登录失败:', loginResult.message)
// 未注册用户openid 已经有了,但 mp-prepay 需要 userId
// 提示用户先注册/登录
if (loginResult.message?.includes('未注册') || loginResult.message?.includes('不存在')) {
Taro.showModal({
title: '需要先登录',
content: '支付前需要先注册账号,是否前往登录?',
confirmText: '去登录',
cancelText: '取消'
}).then(modalRes => {
if (modalRes.confirm) {
Taro.navigateTo({ url: '/passport/login' })
}
})
return // 不继续支付流程
}
}
} catch (loginErr) {
console.warn('[PayPage] 自动登录异常:', loginErr)
// 登录异常但不阻断支付openid 已拿到,尝试继续
}
// 4. 请求后端创建 JSAPI 预支付订单
const prepayRes: any = await request({
url: `/app/subscription/mp-prepay/${detail.id}`,
method: 'POST',
data: { openid },
returnRaw: true
})
console.log('[PayPage] mp-prepay 完整返回:', JSON.stringify(prepayRes))
if (prepayRes?.code !== 0 && prepayRes?.code !== 200) {
throw new Error(prepayRes?.message || '创建支付订单失败')
}
const payParams: JsapiPayParams = prepayRes.data || prepayRes
if (!payParams.timeStamp || !payParams.package || !payParams.paySign) {
throw new Error('支付参数不完整')
}
// 5. 调用微信支付
await Taro.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: (payParams.signType || 'RSA') as any,
paySign: payParams.paySign
})
// 6. 支付成功后通知后端确认
try {
await request({
url: `/app/subscription/mp-confirm/${detail.subscriptionNo}`,
method: 'POST',
data: {}
})
} catch (confirmErr) {
console.warn('支付确认通知失败,等待轮询:', confirmErr)
}
// 7. 显示成功状态
setPaid(true)
Taro.showToast({ title: '支付成功', icon: 'success' })
// 8. 延迟自动跳转到已购产品页面
setTimeout(() => {
Taro.navigateTo({ url: '/user/apps/index' }).catch(() => {
// 如果 navigateTo 失败(比如在 tab 页),回退到用户页
Taro.switchTab({ url: '/pages/user/user' })
})
}, 1500)
} catch (err: any) {
console.error('[PayPage] 支付失败:', err)
console.error('[PayPage] err.name:', err?.name, 'err.type:', err?.type)
console.error('[PayPage] err.code:', err?.code, 'err.message:', err?.message)
if (err?.errMsg?.includes('cancel')) {
Taro.showToast({ title: '支付已取消', icon: 'none' })
} else {
const msg = err?.message || err?.errMsg || '支付失败,请重试'
Taro.showToast({ title: msg, icon: 'error', duration: 2000 })
}
} finally {
setPaying(false)
}
}
// 返回
const handleBack = () => {
Taro.navigateBack({ delta: 1 }).catch(() => {
Taro.switchTab({ url: '/pages/user/user' })
})
}
// 格式化价格描述
const getPriceDesc = () => {
if (!detail) return ''
if (detail.priceType === 'one_time') return '永久买断'
if (detail.priceType === 'subscription') {
return detail.subscriptionPeriod === 'year' ? '按年订阅' : '按月订阅'
}
return ''
}
// --- 加载中 ---
if (loading) {
return (
<View className="pay-page min-h-screen bg-gray-50 flex items-center justify-center">
<View className="text-center">
<Clock className="text-blue-500 mb-4" size="48" />
<Text className="block text-gray-500">...</Text>
</View>
</View>
)
}
// --- 错误 ---
if (errorMsg) {
return (
<View className="pay-page min-h-screen bg-gray-50 flex items-center justify-center">
<View className="text-center p-8">
<Close className="text-red-500 mb-4" size="48" />
<Text className="block text-gray-800 text-lg mb-2"></Text>
<Text className="block text-gray-500 mb-6">{errorMsg}</Text>
<Button type="primary" onClick={handleBack}></Button>
</View>
</View>
)
}
// --- 已支付 ---
if (paid) {
return (
<View className="pay-page min-h-screen bg-gray-50 flex items-center justify-center">
<View className="text-center p-8">
<View className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Check className="text-green-500" size="40" />
</View>
<Text className="block text-green-600 text-xl font-bold mb-2"></Text>
<Text className="block text-gray-500 mb-2">{detail?.productName || '应用订阅'}</Text>
{detail?.payPrice ? (
<Text className="block text-gray-400 mb-6">
¥{Number(detail.payPrice).toFixed(2)}
</Text>
) : null}
<Button type="primary" onClick={handleBack}></Button>
</View>
</View>
)
}
// --- 支付确认 ---
return (
<View className="pay-page min-h-screen bg-gray-50">
<View className="p-4">
{/* 订单信息 */}
<View className="bg-white rounded-lg shadow-sm p-4 mb-4">
<Text className="block text-lg font-bold text-gray-800 mb-3"></Text>
<Cell title="商品名称" description={detail?.productName || '-'} />
<Cell title="价格类型" description={getPriceDesc()} />
<View className="flex items-center justify-between px-4 py-3">
<Text className="text-gray-600"></Text>
<Price
price={detail?.payPrice}
size="large"
thousands
/>
</View>
<Divider />
<View className="flex items-start px-4 py-2">
<Tips className="text-orange-500 mr-2 mt-1" size="16" />
<Text className="text-sm text-gray-500">
使
</Text>
</View>
</View>
{/* 支付按钮 */}
<View className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
<View className="flex gap-3">
<Button
block
onClick={handleBack}
className="flex-1"
>
</Button>
<Button
type="primary"
block
loading={paying}
onClick={handlePay}
className="flex-1"
>
{paying ? '支付中...' : '立即支付'}
</Button>
</View>
</View>
</View>
{/* 底部安全距离占位 */}
<View style={{ height: '100px' }} />
</View>
)
}
export default PayPage

View File

@@ -83,7 +83,7 @@ const ThemeSelector: React.FC = () => {
>
<Text className="text-lg font-bold mb-2"></Text>
<Text className="text-sm opacity-90 px-2">{currentTheme.description}</Text>
<View className="mt-4 flex justify-center space-x-4">
<View className="mt-4 flex justify-center">
<View
className="w-8 h-8 rounded-full"
style={{ backgroundColor: currentTheme.primary }}

View File

@@ -11,7 +11,7 @@ import {
Checkbox,
} from '@nutui/nutui-react-taro'
import {UserVerify} from "@/api/system/userVerify/model";
import {addUserVerify, myUserVerify, updateUserVerify} from "@/api/system/userVerify";
import {addUserVerify, myUserVerify, updateUserVerify, verifyIdCard} from "@/api/system/userVerify";
import {uploadFile} from "@/api/system/file";
function Index() {
@@ -62,7 +62,7 @@ function Index() {
};
// 提交表单
const submitSucceed = (values: any) => {
const submitSucceed = async (values: any) => {
console.log('提交表单', values);
if (FormData.status != 2 && FormData.status != undefined) return false;
@@ -113,6 +113,36 @@ function Index() {
});
return false;
}
// 个人认证:先调用阿里云实人认证进行身份核验
if (FormData.type == 0) {
Taro.showLoading({ title: '身份核验中...' });
try {
const verifyResult = await verifyIdCard(FormData.realName!, FormData.idCard!);
Taro.hideLoading();
if (!verifyResult.isMatch) {
Taro.showToast({
title: '身份信息不一致,请核实后重新填写',
icon: 'none',
duration: 3000
});
return false;
}
// 核验通过,继续提交
} catch (error: any) {
Taro.hideLoading();
console.error('身份核验失败', error);
Taro.showToast({
title: error.message || '身份核验失败,请稍后重试',
icon: 'none',
duration: 3000
});
return false;
}
}
const saveOrUpdate = isUpdate ? updateUserVerify : addUserVerify;
saveOrUpdate({...FormData, status: 0}).then(() => {
Taro.showToast({title: `提交成功`, icon: 'success'})

View File

@@ -47,16 +47,37 @@ export function wxParse(htmlText:string) {
export function copyText(text: string) {
Taro.setClipboardData({
// 微信小程序调用 setClipboardData 成功后,系统会自带「内容已复制」提示。
// 为避免重复 Toast 以及部分机型/场景下的误报 fail小程序端不再额外显示自定义 Toast。
const isWeapp = process.env.TARO_ENV === 'weapp';
if (isWeapp && typeof wx !== 'undefined' && wx.setClipboardData) {
// 小程序端直接调用原生 API避免 Taro Promise 封装可能带来的上下文/时序问题
wx.setClipboardData({
data: text,
success: function () {
// 系统自带 toast无需额外提示
},
fail: function (res: any) {
console.error('[copyText] wx.setClipboardData failed:', res?.errMsg || res);
}
});
return;
}
Taro.setClipboardData({ data: text })
.then(() => {
if (!isWeapp) {
Taro.showToast({
title: '复制成功',
icon: 'success',
duration: 2000
});
},
fail: function () {
}
})
.catch((err: any) => {
console.error('[copyText] Taro.setClipboardData failed:', err?.errMsg || err);
if (!isWeapp) {
Taro.showToast({
title: '复制失败',
icon: 'none',

View File

@@ -78,6 +78,8 @@ const requestInterceptor = (config: RequestConfig): RequestConfig => {
config.header = { ...defaultHeaders, ...config.header };
console.log('[Request] 发送请求:', { url: config.url, method: config.method, hasToken: !!token, returnRaw: config.returnRaw });
// 显示加载提示
if (config.showLoading) {
Taro.showLoading({ title: '加载中...' });
@@ -95,10 +97,8 @@ const responseInterceptor = <T>(response: any, config: RequestConfig): T => {
const { statusCode, data } = response;
// 调试信息(仅开发环境
if (process.env.NODE_ENV === 'development') {
console.log('API Response:', { statusCode, url: config.url, success: statusCode === 200 });
}
// 调试信息(所有环境都打印,方便真机排查
console.log('[Request] 响应:', { statusCode, url: config.url, data: JSON.stringify(data).substring(0, 500) });
// HTTP状态码检查
if (statusCode !== 200) {
@@ -139,6 +139,7 @@ const responseInterceptor = <T>(response: any, config: RequestConfig): T => {
// 认证错误
if (apiResponse.code === 401 || apiResponse.code === 403) {
console.error('[Request] 认证错误:', { code: apiResponse.code, message: apiResponse.message, url: config.url });
handleAuthError();
throw new RequestError(
apiResponse.message || '认证失败',
@@ -149,9 +150,7 @@ const responseInterceptor = <T>(response: any, config: RequestConfig): T => {
}
// 业务错误
if (process.env.NODE_ENV === 'development') {
console.error('API业务错误:', { code: apiResponse.code, message: apiResponse.message });
}
console.error('[Request] 业务错误:', { code: apiResponse.code, message: apiResponse.message, url: config.url });
throw new RequestError(
apiResponse.message || '请求失败',
ErrorType.BUSINESS_ERROR,
@@ -289,8 +288,8 @@ const executeRequest = <T>(config: RequestConfig): Promise<T> => {
// 主请求函数
export async function request<T>(options: RequestConfig): Promise<T> {
try {
// 请求拦截
const config = requestInterceptor({ ...DEFAULT_CONFIG, ...options });
// 拼接完整URL相对路径自动补上 baseUrl
const config = requestInterceptor({ ...DEFAULT_CONFIG, ...options, url: buildUrl(options.url) });
// 执行请求(带重试)
const result = await retryRequest<T>(config);

View File

@@ -1,35 +1,27 @@
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
darkMode: 'media', // or 'media' or 'class'
// 禁用 important 语法,微信小程序不支持 .\! 这样的选择器
content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
theme: {
extend: {},
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
}
}
},
variants: {
// 禁用所有变体,避免生成微信小程序不支持的选择器
},
plugins: [],
corePlugins: {
// 禁用微信小程序不支持的功能
preflight: false, // 禁用默认样式重置
// 禁用包含复杂选择器的插件
space: false, // 禁用 space-x, space-y 等(包含 :not([hidden]) 选择器)
divideWidth: false, // 禁用 divide-x, divide-y 等
divideColor: false,
divideStyle: false,
divideOpacity: false,
// 新增禁用项,解决微信小程序兼容性问题
gap: true, // 禁用 gap 类,因为微信小程序不支持 gap 属性
lineClamp: false, // 禁用 line-clamp 类,微信小程序不支持
textIndent: false, // 禁用 text-indent
writingMode: false, // 禁用 writing-mode
hyphens: false, // 禁用 hyphens
// 禁用所有可能包含问题的变体
visibility: false, // 禁用 visibility 相关类,避免生成 .\!visible
// 禁用伪类和交互变体,避免生成 .active\: 等选择器
scale: false, // 禁用 scale 相关类,避免生成问题选择器
transform: false, // 禁用 transform 相关类
transitionProperty: false, // 禁用 transition 相关类
},
};
// Taro 小程序端不需要 preflight浏览器重置样式
preflight: false
}
}

3
types/global.d.ts vendored
View File

@@ -69,6 +69,9 @@ declare const API_BASE_URL: string;
declare const APP_NAME: string;
declare const DEBUG: string;
// 微信小程序原生全局对象 wx
declare const wx: any;
// 基础类型定义
declare global {
/** 通用ID类型 */