准备 AI重建
This commit is contained in:
1 parent
c36f2d60c2
commit
3429c8b402
1487 files changed
+5307
-3208
No files matched your search
@@ -0,0 +1,250 @@
|
||||
---
|
||||
name: personal-virtual-payment
|
||||
description: Guide for integrating WeChat Mini Program virtual payment for personal-entity developers (individual, resident ID, utility category). Covers activation, item purchase (short_series_goods), delivery push, order query, refund, settlement and fees. Trigger when the user asks to enable or integrate virtual payment for a personal mini program, or configure wx.requestVirtualPayment. 个人主体小程序虚拟支付接入指引。
|
||||
---
|
||||
|
||||
# 个人主体小程序虚拟支付接入
|
||||
|
||||
为进一步支持个人开发者实现可持续经营,激发个人主体的创新活力,微信小程序平台将在符合条件的类目范围内,向个人主体小程序开放虚拟支付能力。
|
||||
|
||||
## Overview
|
||||
|
||||
本技能为**个人主体**小程序提供虚拟支付接入流程,覆盖从开通到上线的完整闭环:
|
||||
|
||||
- 核对开通条件(个人主体、工具类目、认证备案)
|
||||
- MP 后台开通虚拟支付并获取关键参数
|
||||
- 道具直购接入(`wx.requestVirtualPayment`)
|
||||
- 服务端签名、发货推送、查单兜底
|
||||
- 退款、结算、费率、发票说明
|
||||
- 部署前检查清单验收
|
||||
|
||||
**适用范围**:仅个人主体小程序,服务类目含「工具」,月支付限额 10 万元。
|
||||
|
||||
## 1. 开通条件核对
|
||||
|
||||
| 条件 | 说明 |
|
||||
| --- | --- |
|
||||
| 个人主体 | 小程序主体为个人,开发者持有居民身份证 |
|
||||
| 服务类目 | 小程序服务类目含「工具」 |
|
||||
| 认证与备案 | 已完成小程序认证、备案 |
|
||||
|
||||
> **限额**:个人主体小程序**月支付限额为 10 万元**(含 Android、iOS 等所有终端)。
|
||||
|
||||
## 2. 开通流程(MP 后台)
|
||||
|
||||
打开浏览器登录微信开放平台(mp.weixin.qq.com):
|
||||
|
||||
1. 左侧栏找到并点击【支付与交易 - 虚拟支付】
|
||||
2. 点击【开通】→ 勾选同意协议 → 填写个人资料(个人身份信息、提现账户、支付管理员)
|
||||
3. 提交后等待审核,一般为 5 分钟内
|
||||
4. 刷新页面、确认审核通过后,扫码签约
|
||||
|
||||
### 开通后获取 3 个关键信息
|
||||
|
||||
| 信息 | 它是什么 | 在哪找 |
|
||||
| --- | --- | --- |
|
||||
| AppID | 小程序的「身份证号」 | MP 后台 → 设置 |
|
||||
| OfferID | 你的支付账号 | 虚拟支付 → 基本配置 |
|
||||
| 现网AppKey | 支付密钥 | 虚拟支付 → 基本配置 |
|
||||
|
||||
同时,需在【道具管理】里创建商品(道具),记下道具 ID 和价格,并完成发布。
|
||||
|
||||
### iOS 支付额外条件
|
||||
|
||||
如需在 iOS 端使用支付:
|
||||
1. **配置小程序简称**(Apple 支付需展示 display name)。配置入口:mp.weixin.qq.com → 虚拟支付 → 基础配置
|
||||
2. 配置后,在虚拟支付 → 基本配置中**开通苹果 IAP 支付**
|
||||
|
||||
## 3. 开发接入
|
||||
|
||||
### 3.1 支付与现有业务流程串接
|
||||
|
||||
虚拟支付只是把「收钱」环节接入小程序已有业务流程。整体闭环:
|
||||
|
||||
```text
|
||||
用户打开小程序 → 登录 → 选择要购买的功能/道具 → 点击「购买」
|
||||
↓
|
||||
① 前端请求服务器下单 → 服务器生成唯一单号 outTradeNo,保存订单(状态:待支付)
|
||||
↓
|
||||
② 服务器返回 payData → 前端调用 wx.requestVirtualPayment 拉起支付
|
||||
↓
|
||||
③ 支付成功后,服务器通过两条路径确认并完成发货:
|
||||
路径 A:收到平台「发货推送」→ 直接发货
|
||||
路径 B:推送丢失时,定时调用 query_order 查单 → 查到已支付就补发货
|
||||
↓
|
||||
④ 前端查询自己服务器的订单状态 → 展示「购买成功」并开放权益
|
||||
```
|
||||
|
||||
关键点:
|
||||
- `outTradeNo`:商家自己生成的业务单号,下单时必填,须唯一
|
||||
- `wx_order_id`:平台返回的单号,跟踪订单、发货、对账以此为准
|
||||
- 发货以「发货推送」为主,推送丢失时用 `query_order` 查单兜底;**前端支付回调不作为发货依据**
|
||||
|
||||
### 3.2 前端接入(小程序端)
|
||||
|
||||
调用基础库接口 `wx.requestVirtualPayment`(签名 = 防伪校验)。payData 由服务端生成并签名。
|
||||
|
||||
```javascript
|
||||
function buyGoods(payData) {
|
||||
wx.requestVirtualPayment({
|
||||
...payData,
|
||||
success(res) { wx.showToast({ title: '支付成功' }); },
|
||||
fail(err) { wx.showToast({ title: '支付失败', icon: 'none' }); }
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**iOS 版本限制**:iOS 端需微信客户端 **8.0.68 及以上版本**。调用支付前先校验,不满足时提示用户更新微信至最新版。
|
||||
|
||||
**payData 核心字段**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| signData | 支付参数 JSON 字符串(服务端生成): |
|
||||
| ├ offerId | OfferID(虚拟支付商户号) |
|
||||
| ├ buyQuantity | 购买数量 |
|
||||
| ├ env | 环境标识,固定填 0 |
|
||||
| ├ currencyType | 币种,固定填 CNY |
|
||||
| ├ productId | 道具 ID |
|
||||
| ├ goodsPrice | 道具单价(分),需与后台道具价格一致 |
|
||||
| ├ outTradeNo | 业务单号(必填,须唯一,如 `T` + 时间戳 + 随机数) |
|
||||
| └ attach | 透传数据(必填,发货时原样透传) |
|
||||
| mode | 固定 `short_series_goods`(道具直购) |
|
||||
| paySig | 服务端用 AppKey 对 `requestVirtualPayment&signData` 做 HMAC-SHA256 |
|
||||
| signature | 服务端用 sessionKey 对 signData 做 HMAC-SHA256(用户态签名) |
|
||||
|
||||
### 3.3 服务端接入
|
||||
|
||||
#### 3.3.1 接收发货推送
|
||||
|
||||
- 需先部署「消息推送接收」能力。使用**腾讯云开发**时,可借助微信开发者工具 Skills 自动完成该部分部署与配置;其余服务端逻辑(下单、查单、发货)由代码实现
|
||||
- 在 MP 后台【虚拟支付 → 基本配置 → 基础配置 → 发货推送配置】填服务器 URL
|
||||
|
||||
推送核心字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| Event | 固定 `xpay_goods_deliver_notify` |
|
||||
| OpenId | 用户 openid(确定发货给谁) |
|
||||
| OutTradeNo | 商家下单时传入的业务单号 |
|
||||
| WeChatPayInfo.MchOrderNo | 平台单号 wx_order_id(跟踪订单、幂等去重,以此为准) |
|
||||
| GoodsInfo.ProductId | 道具 ID |
|
||||
| GoodsInfo.Quantity | 购买数量(按数量发放) |
|
||||
|
||||
服务器处理逻辑:
|
||||
1. 解析 XML,取平台单号 `wx_order_id`(位于 `WeChatPayInfo.MchOrderNo` 字段)
|
||||
2. **幂等**(同一订单重复通知只发一次货):查本地订单表,若该 wx_order_id 已发货则直接返回成功
|
||||
3. 发货:根据 `OpenId` 和道具 ID,给对应用户发放对应数量,更新订单状态
|
||||
4. 返回 `0` 表示成功,否则平台会重试(最多 15 次)
|
||||
|
||||
应答格式:
|
||||
|
||||
```xml
|
||||
<xml><ErrCode>0</ErrCode><ErrMsg><![CDATA[success]]></ErrMsg></xml>
|
||||
```
|
||||
|
||||
#### 3.3.2 查询订单(兜底发货)
|
||||
|
||||
接口:`POST /xpay/query_order`(带 pay_sig 签名)
|
||||
|
||||
```json
|
||||
{
|
||||
"openid": "用户openid",
|
||||
"env": 0,
|
||||
"order_id": "业务订单号 outTradeNo"
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ 参数名是 `order_id`(开发者传入的 outTradeNo),不是 `out_trade_no`。
|
||||
|
||||
用于:推送丢失时主动查单补发货,建议定时每 5 分钟查一次,查到已支付就发货。
|
||||
|
||||
#### 3.3.3 服务端接口清单
|
||||
|
||||
| 接口 | 作用 |
|
||||
| --- | --- |
|
||||
| `POST /pay/order` | 生成 outTradeNo + signData + paySig,返回给前端去支付 |
|
||||
| `POST /pay/notify` | 接收发货推送,验签后发货,返回 XML |
|
||||
| `POST /pay/query` | 转发调用 query_order,供兜底查单 |
|
||||
|
||||
### 3.4 签名算法(两套签名)
|
||||
|
||||
#### 支付签名 paySig(服务端算,AppKey 是密钥)
|
||||
|
||||
```python
|
||||
import hmac, hashlib
|
||||
|
||||
def calc_pay_sig(uri, post_body, appkey):
|
||||
"""uri 不带问号参数。
|
||||
- C 端(wx.requestVirtualPayment 下单):uri 固定为 requestVirtualPayment
|
||||
- B 端(服务端 /xpay/* 接口):uri 为实际接口路径,如 /xpay/query_order
|
||||
"""
|
||||
msg = uri + '&' + post_body # post_body 是真正发出去的原始字符串
|
||||
return hmac.new(appkey.encode('utf-8'), msg.encode('utf-8'),
|
||||
hashlib.sha256).hexdigest()
|
||||
```
|
||||
|
||||
#### 用户态签名 signature(服务端算,sessionKey 是密钥)
|
||||
|
||||
```python
|
||||
def calc_signature(post_body, session_key):
|
||||
return hmac.new(session_key.encode('utf-8'), post_body.encode('utf-8'),
|
||||
hashlib.sha256).hexdigest()
|
||||
```
|
||||
|
||||
## 4. 退款、结算与订单查询
|
||||
|
||||
### 4.1 退款
|
||||
|
||||
| 终端 | 退款方式 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Android 等 | 开发者主动退款 | 在 MP 后台【虚拟支付 → 交易订单】操作退款,或用 `refund_order` 接口发起;退款完成后收到 `xpay_refund_notify` 推送 |
|
||||
| iOS | 用户向 App Store 申请 | 开发者无法主动退款;用户申请后由 Apple 决定,退款成功收到 `xpay_refund_notify` |
|
||||
|
||||
> 手续费规则:支付时间 180 天以内的退款,平台退还手续费;超过 180 天的退款,平台不退还手续费。
|
||||
|
||||
### 4.2 结算
|
||||
|
||||
| 终端 | 结算周期 | 费率 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| Android 等 | T+3 | 1% | 腾讯技术服务费 |
|
||||
| iOS | 约 45-60 天 | 12% | Apple 佣金 |
|
||||
|
||||
- **提现**:资金到账后,可在 MP 后台【虚拟支付】查看账户余额、每日账单并发起提现
|
||||
- **发票**:可在**次月 5 号后**申请开具上个月的腾讯技术服务费发票
|
||||
|
||||
### 4.3 订单查询
|
||||
|
||||
- 页面查询:MP 后台【虚拟支付 → 交易订单】,可按支付渠道切换「普通支付 / Apple 支付」
|
||||
- 接口查询:`query_order`(见 3.3.2),可用于核对订单与退款状态
|
||||
|
||||
## 5. Agent 注意事项
|
||||
|
||||
1. **用户告知**:向用户确认开放条件(个人主体、工具类目、认证备案)与**月支付限额 10 万元**,并说明退款、结算与费率规则(见第四节)
|
||||
2. **接入范围**:只做道具直购,不引入代币等额外功能
|
||||
3. **outTradeNo**:每次下单重新生成、保证唯一(8-32 位,不能以下划线开头),不能复用
|
||||
4. **签名**:严格按 3.4 节实现;`post_body` 必须与实际请求体完全一致(不格式化、不改键顺序);`appkey` 用正式 AppKey;`sessionKey` 通过 `auth.code2Session` 获取
|
||||
5. **发货**:以 `wx_order_id` 做幂等去重;`success` 回调可能丢失(用户异常退出会丢),以「发货推送」为主、`query_order` 查单兜底
|
||||
6. **金额与 env**:金额单位是「分」,全程不要换算;`env` 固定填 `0`(正式环境/现网)
|
||||
|
||||
## 6. 部署前检查清单
|
||||
|
||||
- [ ] 开放条件确认:个人主体 + 中国大陆居民身份证 + 服务类目为「工具」+ 已完成认证、备案
|
||||
- [ ] 已了解并告知用户:全终端月支付限额为 10 万元
|
||||
- [ ] MP 后台虚拟支付已开通
|
||||
- [ ] 已拿到 AppID / OfferID / 现网AppKey
|
||||
- [ ] 若需 iOS 支付:已配置小程序简称
|
||||
- [ ] 消息推送接收已部署(腾讯云开发可用开发者工具 Skills 自动完成)
|
||||
- [ ] MP 后台已配置发货推送 URL,并完成一笔测试支付验证
|
||||
- [ ] 服务器验签逻辑通过(签名函数与官方文档签名示例核对一致)
|
||||
- [ ] 发货推送已做幂等(以 wx_order_id 去重)
|
||||
- [ ] 前端 wx.requestVirtualPayment 打通,支付成功并收到发货推送
|
||||
- [ ] 兜底查单逻辑已就绪(query_order)
|
||||
- [ ] 已向用户说明退款规则、结算周期与费率(Android 1%、iOS 12%)
|
||||
- [ ] 上线后用小额真单验证:支付 → 推送 → 发货 → 后台账单金额一致
|
||||
|
||||
## 参考资源
|
||||
|
||||
- 个人主体接入指引:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/person
|
||||
- 客户端 API:https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestVirtualPayment.html
|
||||
- 小程序简称配置:https://developers.weixin.qq.com/community/develop/doc/0004221455ce78d413893d4b75b009
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "538640",
|
||||
"slug": "miniprogram-virtualpay-person",
|
||||
"version": "1.0.2",
|
||||
"publishedAt": 1788233178091
|
||||
}
|
||||
+872
@@ -0,0 +1,872 @@
|
||||
# 「叶雷讲数学」小程序 重构开发文档
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 版本 | v1.0 |
|
||||
| 日期 | 2026-09-29 |
|
||||
| 状态 | 设计定稿,待开发 |
|
||||
| 范围 | 小程序端 / 服务端 / 管理后台 / 数据端 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
现有系统为「微信小程序 + 原生 PHP + MySQL 5.7 + 静态文件站点」的实现,可正常运行,但存在结构性问题:无鉴权体系、SQL 全字符串拼接、数据库无主键与约束、无正式管理后台、无支付能力、存在大量死代码与废弃表、服务端无版本控制。
|
||||
|
||||
本次重构的目标是:**在保持小程序用户侧外观与使用体验不变的前提下,把技术实现整体替换为规范化工程,并补齐支付与管理后台能力。**
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
1. 用户侧无感升级:页面表现一致、已有数据零丢失、已购课程权限延续。
|
||||
2. 技术实现规范化:分层、类型安全、边界校验、迁移管理、统一契约。
|
||||
3. 补齐支付能力,使课程销售可自助完成。
|
||||
4. 补齐管理后台,使运营脱离手写 SQL。
|
||||
|
||||
### 1.3 明确不做
|
||||
|
||||
- 不做微服务、K8s、服务网格。
|
||||
- 不上 Redis / 消息队列(发货用「数据库状态机 + 定时查单」足够)。
|
||||
- 不上 GraphQL。
|
||||
- 不引入 Turborepo / Nx。
|
||||
- 不重写小程序为 Taro / uni-app。
|
||||
- 不补短信能力(触达改用微信订阅消息)。
|
||||
- 不做 DDD 分层、CQRS、事件溯源。
|
||||
|
||||
---
|
||||
|
||||
## 2. 决策记录(ADR)
|
||||
|
||||
| 编号 | 决策 | 理由 | 影响 |
|
||||
|---|---|---|---|
|
||||
| ADR-001 | 放弃口令码,权益由订单驱动 | 支付上线后口令码仅剩线下分发用途,维护成本高于价值 | 删除 `课程口令码`、`课程口令码读取信息`;老用户走 legacy 权益迁移 |
|
||||
| ADR-002 | 数据端保留 MySQL 5.7,不升级 | 用户明确要求 | Prisma `provider = mysql`;见 4.1 约束清单 |
|
||||
| ADR-003 | 服务端与管理后台使用独立域名 | 用户要求 | Nginx 分站点;后端仍为同一 Node 进程 |
|
||||
| ADR-004 | 小程序前端不重写,只重写 JS 层 | 保证「外观不变」 | 复用全部 `.wxml` / `.wxss` / `static/` |
|
||||
| ADR-005 | `VideoDetailsPage` 保持现有实现逻辑不变 | 该页为对外宣传页 | 内容继续硬编码在前端,不迁入数据库 |
|
||||
| ADR-006 | AppID 沿用 `wx3fbaf897825bf04f` | 换 AppID 等于重发小程序,用户与虚拟支付配置全部丢失 | 数据迁移无需做 openid 映射 |
|
||||
| ADR-007 | 技术栈换为 Node + NestJS + Prisma | 用户选定 Node;管理后台与 API 可共享类型与校验 | 需自行维护 Node 进程守护与部署 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
### 3.1 架构图
|
||||
|
||||
```text
|
||||
微信小程序
|
||||
| https api.yeomai.site/v1/*
|
||||
| Authorization: Bearer <JWT>
|
||||
v
|
||||
+----------------------------------------------------+
|
||||
| NestJS 单进程 :3000 |
|
||||
| + /v1/* 小程序端接口 |
|
||||
| + /admin/v1/* 管理后台接口(独立 JWT) |
|
||||
| + /pay/notify 虚拟支付发货推送(XML 应答) |
|
||||
| + @nestjs/schedule 每 5 分钟兜底查单 |
|
||||
+----------------------------------------------------+
|
||||
| | |
|
||||
v v v
|
||||
MySQL 5.7 对象存储 / CDN 微信虚拟支付
|
||||
(宝塔) static.yeomai.site 下单 / 推送 / 查单
|
||||
|
||||
管理后台(Next.js :3001)
|
||||
admin.yeomai.site -> 后台前端
|
||||
admin-api.yeomai.site -> 反代到同一 Node 进程 /admin/v1/*
|
||||
```
|
||||
|
||||
### 3.2 域名规划
|
||||
|
||||
| 域名 | Nginx 指向 | 用途 |
|
||||
|---|---|---|
|
||||
| `api.yeomai.site` | `127.0.0.1:3000`(`/v1/*`) | 小程序接口 |
|
||||
| `admin-api.yeomai.site` | `127.0.0.1:3000`(`/admin/v1/*`) | 管理后台接口 |
|
||||
| `admin.yeomai.site` | `127.0.0.1:3001` | 管理后台前端 |
|
||||
| `static.yeomai.site` | 对象存储 / CDN | 图片与 HLS 视频 |
|
||||
|
||||
> `api.yeomai.site` 沿用现有域名,小程序「request 合法域名」白名单无需改动。
|
||||
|
||||
### 3.3 小程序侧 MP 配置变更清单
|
||||
|
||||
| 项 | 是否需要改 | 说明 |
|
||||
|---|---|---|
|
||||
| `request` 合法域名 | 沿用则不用改 | 仅 `wx.request` / `uploadFile` / `downloadFile` / `connectSocket` 受白名单约束 |
|
||||
| `<image>` / `<video>` 资源域名 | **不需要** | 组件引用的图片与音视频资源不受合法域名限制,故视频迁 CDN 无需改 MP 配置 |
|
||||
| 虚拟支付发货推送 URL | 需要 | 填在【虚拟支付 → 基本配置】,非域名白名单 |
|
||||
| 小程序简称 | iOS 支付需要 | 虚拟支付 → 基础配置 |
|
||||
| 苹果 IAP 支付开关 | iOS 支付需要 | 虚拟支付 → 基础配置 |
|
||||
|
||||
### 3.4 宝塔部署拓扑
|
||||
|
||||
**软件依赖**:MySQL 5.7(沿用)、Node 版本管理器(Node 22 LTS)、Nginx。
|
||||
|
||||
**站点**:`api` / `admin-api` / `admin` 三个站点,各配 Let's Encrypt 证书。
|
||||
|
||||
**进程**:api 与 admin 分别用宝塔「Node 项目」+ PM2 守护。
|
||||
|
||||
**定时任务**:兜底查单使用进程内 `@nestjs/schedule`,不额外依赖系统 cron。数据库备份使用宝塔计划任务 `mysqldump`。
|
||||
|
||||
> ⚠️ **阶段 0 必须先验证**:宝塔的 Node 项目管理器对 pnpm workspace 支持有限。备选方案为「本地或 CI 构建后仅部署产物」,或将 `apps/api` 配置为独立启动目录。此项若不通,会阻塞整个部署流程。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据端设计
|
||||
|
||||
### 4.1 MySQL 5.7 约束清单
|
||||
|
||||
保留 5.7 意味着以下限制必须写进开发规范:
|
||||
|
||||
| 约束 | 影响 | 应对 |
|
||||
|---|---|---|
|
||||
| 无 `CHECK` 约束(语法可解析但不生效) | 不能用数据库层做值域校验 | 校验下沉到应用层(zod);关键枚举值靠代码约束 |
|
||||
| 无 `utf8mb4_0900_ai_ci`(8.0 才有) | 排序规则需换 | 统一使用 `utf8mb4_general_ci` |
|
||||
| 无窗口函数 | 统计类 SQL 受限 | 统计数据在应用层聚合 |
|
||||
| 单列索引键长限制 | 若行格式为 COMPACT/REDUNDANT,键上限 767 字节,utf8mb4 下 `VARCHAR(255)` 建唯一索引会失败 | 所有需建索引的字符字段长度控制在 `VARCHAR(64)` 以内 |
|
||||
| 已 EOL(2023-10) | 无安全补丁 | 接受风险;必须保证每日备份可用 |
|
||||
|
||||
> ⚠️ **Prisma 与 5.7 的实际坑**:`prisma migrate dev` 需要创建 shadow database,要求数据库账号具备 `CREATE DATABASE` 权限。宝塔创建的 MySQL 用户默认没有该权限。
|
||||
>
|
||||
> 解决方案(二选一):
|
||||
> 1. 为迁移专用账号授予 `CREATE` / `DROP` 权限(推荐,本机开发用)。
|
||||
> 2. 在本地生成迁移 SQL(`prisma migrate diff`),生产端仅执行 `prisma migrate deploy`。
|
||||
|
||||
### 4.2 表清单与映射:20 张 → 16 张
|
||||
|
||||
| 旧表 | 新表 | 处理 |
|
||||
|---|---|---|
|
||||
| `用户信息` (284) | `users` | 迁移,结构重写 |
|
||||
| `用户已购课程` (48) | `entitlements` | 迁移为 `source = legacy` |
|
||||
| `课程口令码读取信息` (10) | `courses` | 升级为正式课程表 |
|
||||
| `视频课程数据` (841) | `lessons` | 迁移 |
|
||||
| `免费视频课程数据` | `free_lessons` | 迁移 |
|
||||
| `首页课程列表` (5) | `home_courses` | 迁移 |
|
||||
| `首页免费课程列表` (3) | `home_free_courses` | 迁移 |
|
||||
| `首页横幅图片` (3) | `banners` | 迁移 |
|
||||
| `网页列表` (2) | `articles` | 迁移 |
|
||||
| `课程页面Tips` (2) | `course_tips` | 迁移 |
|
||||
| `cloudconfig` (13) | `settings` | 迁移,数字 ID 改字符串 key |
|
||||
| `课程口令码` (5) | `legacy_redeem_codes` | **保留原样**,见 9.4 |
|
||||
| `网页数据` | — | 废弃 |
|
||||
| `课程页面Tips2` | — | 废弃 |
|
||||
| `首页课程列表2` | — | 废弃 |
|
||||
| `首页免费课程列表2` | — | 废弃 |
|
||||
| `首页横幅图片2` | — | 废弃 |
|
||||
| `网页列表2` | — | 废弃 |
|
||||
| `免费视频课程数据2` | — | 废弃 |
|
||||
| `验证码发送` | — | 废弃(不做短信) |
|
||||
| — | `orders` | 新增 |
|
||||
| — | `payment_notify_log` | 新增 |
|
||||
| — | `products` | 新增 |
|
||||
| — | `admin_users` | 新增 |
|
||||
|
||||
### 4.3 通用规范
|
||||
|
||||
- 表名、字段名一律英文 `snake_case`。
|
||||
- 主键统一 `id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY`。
|
||||
- 时间统一 `DATETIME`,**存 UTC**,展示层转 `Asia/Shanghai`。
|
||||
- 金额统一 `INT UNSIGNED`,单位**分**,全程不换算。
|
||||
- 状态字段用 `TINYINT` + 代码内枚举,不使用中文字符串。
|
||||
- 字符集 `utf8mb4`,排序规则 `utf8mb4_general_ci`。
|
||||
- 外键约束一律不建(由应用层保证),避免迁移与批量操作受限。
|
||||
|
||||
### 4.4 关键表结构
|
||||
|
||||
#### users
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| openid | VARCHAR(64) | NOT NULL UNIQUE | 微信 openid,最长 28 字符 |
|
||||
| nickname | VARCHAR(32) | NULL | 用户名,前端限制 8 字符 |
|
||||
| headicon_id | INT UNSIGNED | NOT NULL DEFAULT 0 | 头像序号 |
|
||||
| wx_session_key | VARCHAR(255) | NULL | 加密存储,**虚拟支付签名必需** |
|
||||
| session_key_updated_at | DATETIME | NULL | 用于判断是否需重新登录刷新 |
|
||||
| status | TINYINT | NOT NULL DEFAULT 1 | 1 正常 / 0 封禁 |
|
||||
| registered_at | DATETIME | NOT NULL | |
|
||||
| last_login_at | DATETIME | NULL | |
|
||||
| created_at / updated_at | DATETIME | NOT NULL | |
|
||||
|
||||
> `wx_session_key` 必须加密存储(AES-256-GCM,密钥来自环境变量)。明文存储等同于泄露用户态签名能力。
|
||||
|
||||
#### courses
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| code | VARCHAR(64) | NOT NULL UNIQUE | **保留原 64 位 sha256 串原值** |
|
||||
| title | VARCHAR(128) | NOT NULL | |
|
||||
| intro | VARCHAR(255) | NULL | |
|
||||
| cover_image | VARCHAR(255) | NULL | 封面文件名 |
|
||||
| price_fen | INT UNSIGNED | NOT NULL DEFAULT 0 | 展示价,实际以 `products` 为准 |
|
||||
| status | TINYINT | NOT NULL DEFAULT 1 | 0 下架 / 1 上架 |
|
||||
| sort_order | INT | NOT NULL DEFAULT 0 | |
|
||||
| created_at / updated_at | DATETIME | NOT NULL | |
|
||||
|
||||
> 🔴 **`code` 字段绝对不能改写。** 它同时是视频目录名(`Course/{code}/{chapter}/{play}/playlist.m3u8`)与封面名(`Course/{code}.png`),一旦改动,全部付费媒体资源立即失效。
|
||||
|
||||
#### lessons(付费课时)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| course_id | BIGINT UNSIGNED | NOT NULL | → courses.id |
|
||||
| play_id | VARCHAR(64) | NOT NULL | **原值保留**,参与视频路径 |
|
||||
| chapter_id | VARCHAR(64) | NOT NULL | **原值保留**,参与视频路径 |
|
||||
| chapter_title | VARCHAR(255) | NULL | |
|
||||
| lesson_title | VARCHAR(255) | NULL | |
|
||||
| duration | VARCHAR(16) | NULL | 原为字符串,保留 |
|
||||
| published_at | DATETIME | NULL | |
|
||||
| cover_image | VARCHAR(255) | NULL | |
|
||||
| sort_order | INT | NOT NULL DEFAULT 0 | |
|
||||
|
||||
索引:`UNIQUE(course_id, chapter_id, play_id)`、`INDEX(course_id, sort_order)`。
|
||||
|
||||
#### entitlements(权益 — 播放鉴权唯一入口)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | → users.id |
|
||||
| course_id | BIGINT UNSIGNED | NOT NULL | → courses.id |
|
||||
| source | TINYINT | NOT NULL | 1 订单 / 2 老数据 / 3 后台发放 / 4 旧口令码 |
|
||||
| ref_id | BIGINT UNSIGNED | NULL | 订单 id 或操作人 id |
|
||||
| granted_at | DATETIME | NOT NULL | |
|
||||
| expire_at | DATETIME | NULL | NULL 表示永久 |
|
||||
| note | VARCHAR(255) | NULL | |
|
||||
|
||||
索引:`UNIQUE(user_id, course_id)`、`INDEX(user_id)`。
|
||||
|
||||
> 抽掉口令码后,权益来源仍有三种(订单 / 老数据 / 后台发放)。**播放鉴权只查这一张表**,避免三条分支三处 bug。
|
||||
|
||||
#### orders
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| out_trade_no | VARCHAR(32) | NOT NULL UNIQUE | 本地业务单号 |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | |
|
||||
| course_id | BIGINT UNSIGNED | NOT NULL | |
|
||||
| product_id | VARCHAR(64) | NOT NULL | 微信虚拟支付道具 ID |
|
||||
| quantity | INT UNSIGNED | NOT NULL DEFAULT 1 | |
|
||||
| amount_fen | INT UNSIGNED | NOT NULL | |
|
||||
| status | TINYINT | NOT NULL | 0 待支付 / 1 已支付 / 2 已发货 / 3 已退款 / 4 已关闭 |
|
||||
| env | TINYINT | NOT NULL DEFAULT 0 | 固定 0 |
|
||||
| wx_order_id | VARCHAR(64) | NULL UNIQUE | 平台单号,**幂等依据** |
|
||||
| attach | VARCHAR(255) | NULL | 透传字段 |
|
||||
| created_at / paid_at / delivered_at / refunded_at | DATETIME | NULL | |
|
||||
|
||||
索引:`INDEX(user_id, created_at)`、`INDEX(status, created_at)`。
|
||||
|
||||
#### payment_notify_log
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| wx_order_id | VARCHAR(64) | |
|
||||
| out_trade_no | VARCHAR(32) | |
|
||||
| event | VARCHAR(64) | 如 `xpay_goods_deliver_notify` |
|
||||
| raw_body | MEDIUMTEXT | 原始 XML,用于重放与排查 |
|
||||
| processed | TINYINT | |
|
||||
| err_msg | VARCHAR(255) | |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
索引:`INDEX(wx_order_id)`。
|
||||
|
||||
#### products
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| course_id | BIGINT UNSIGNED | → courses.id |
|
||||
| wx_product_id | VARCHAR(64) UNIQUE | MP 后台道具 ID |
|
||||
| name | VARCHAR(128) | |
|
||||
| price_fen | INT UNSIGNED | 必须与 MP 后台道具价格一致 |
|
||||
| status | TINYINT | |
|
||||
|
||||
#### admin_users
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| username | VARCHAR(64) UNIQUE | |
|
||||
| password_hash | VARCHAR(255) | argon2id |
|
||||
| display_name | VARCHAR(64) | |
|
||||
| role | TINYINT | 1 超管 / 2 运营 |
|
||||
| totp_secret | VARCHAR(64) NULL | 二次验证 |
|
||||
| status | TINYINT | |
|
||||
| last_login_at | DATETIME | |
|
||||
|
||||
#### legacy_redeem_codes(旧口令码留存)
|
||||
|
||||
原样保留 5 条记录,用于凭证备查与人工核销,不参与任何自动流程。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| code | VARCHAR(64) UNIQUE | 原口令码 |
|
||||
| course_id | BIGINT UNSIGNED | → courses.id |
|
||||
| remaining_uses | INT | 原剩余有效次数 |
|
||||
| expires_at | DATETIME NULL | 原口令结束日期 |
|
||||
| note | VARCHAR(255) | 备注 |
|
||||
|
||||
#### settings
|
||||
|
||||
数字 ID 迁移为字符串 key,附映射关系(见 9.3)。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| key | VARCHAR(64) UNIQUE | 如 `site.version` |
|
||||
| value | VARCHAR(255) | |
|
||||
| remark | VARCHAR(255) | 原「注释」字段 |
|
||||
|
||||
#### 内容类表(结构相近)
|
||||
|
||||
`home_courses`、`home_free_courses`、`banners`、`articles`、`course_tips`、`free_lessons` 均为「内容 + 排序 + 状态」结构,统一包含:
|
||||
|
||||
```text
|
||||
id, <业务字段...>, cover_image/filename, sort_order, status, created_at, updated_at
|
||||
```
|
||||
|
||||
其中 `articles` 额外包含 `legacy_page_id VARCHAR(32)`。
|
||||
|
||||
> 🔴 **封面与图片文件名迁移时一律原样保留。** 原因:小程序里的图片地址是拼出来的——
|
||||
>
|
||||
> - 首页新课:`Images/NewCourse/{课程名称}.png`
|
||||
> - 首页试听:`Images/FreeCourse/{课程名称}.png`
|
||||
> - 轮播:`Images/Banner/{图片名}`
|
||||
> - 知识速通:`WebPages/{网页id}.png`
|
||||
> - 付费课封面:`Course/{course.code}.png`
|
||||
>
|
||||
> 任何字段被「规范化改写」,对应图片立刻 404。
|
||||
|
||||
### 4.5 首页内容块与课程的对应关系
|
||||
|
||||
注意首页「新课上线」与付费课程**不是一一对应**:
|
||||
|
||||
| 首页新课条目(5 条) | 对应付费课程 |
|
||||
|---|---|
|
||||
| 几何模型封面 | 几何模型 |
|
||||
| 中考数学压轴大招封面 | 中考数学压轴大招 |
|
||||
| 七年级同步课程封面2 | 7年级同步课程 |
|
||||
| 八年级同步课程 | 8年级同步课程 |
|
||||
| 九年级同步课程 | 9年级同步课程 |
|
||||
|
||||
以下 5 门课程**不在首页展示**,仅在「知识」页与购买入口出现:7/8/9 年级数学同步视频课(北师大)、梁老师中考数学一轮/二轮满分冲刺。
|
||||
|
||||
`home_courses` 需要新增 `course_id` 可空字段,建立「首页长图页 → 可购买课程」的关联,用于挂载购买按钮。
|
||||
|
||||
---
|
||||
|
||||
## 5. 服务端设计
|
||||
|
||||
### 5.1 技术栈
|
||||
|
||||
| 项 | 选型 |
|
||||
|---|---|
|
||||
| 运行时 | Node 22 LTS |
|
||||
| 语言 | TypeScript(strict,禁止 `any`) |
|
||||
| 框架 | NestJS |
|
||||
| ORM / 迁移 | Prisma(`provider = mysql`) |
|
||||
| 校验 | zod(与管理后台共享 `packages/shared`) |
|
||||
| 鉴权 | `@nestjs/jwt` + argon2 |
|
||||
| 日志 | pino |
|
||||
| 定时任务 | `@nestjs/schedule` |
|
||||
| 文档 | Swagger 自动生成 |
|
||||
| 测试 | vitest + supertest |
|
||||
| 仓库 | pnpm workspaces 单仓 |
|
||||
|
||||
### 5.2 目录结构
|
||||
|
||||
```text
|
||||
apps/api/src/
|
||||
├─ modules/
|
||||
│ ├─ auth/ code2Session、sessionKey 托管、JWT 签发
|
||||
│ ├─ users/
|
||||
│ ├─ courses/ 课程与课时
|
||||
│ ├─ entitlements/ 权益(播放鉴权唯一入口)
|
||||
│ ├─ home/ banner / 新课 / 试听 / 文章 / 配置
|
||||
│ ├─ orders/ 下单、订单查询
|
||||
│ ├─ payments/ 签名、发货推送、兜底查单
|
||||
│ ├─ media/ 签名播放地址签发
|
||||
│ └─ admin/ 管理后台接口(独立守卫)
|
||||
├─ legacy/ /API/*.php 兼容层(过渡用,阶段 8 删除)
|
||||
├─ common/ 统一响应拦截器 / 异常过滤器 / 守卫 / 装饰器
|
||||
└─ main.ts
|
||||
```
|
||||
|
||||
### 5.3 接口清单
|
||||
|
||||
#### 小程序端 `/v1/*`
|
||||
|
||||
| 方法 | 路径 | 鉴权 | 说明 |
|
||||
|---|---|---|---|
|
||||
| POST | `/v1/auth/wx-login` | 否 | code 换 token,同时托管 sessionKey |
|
||||
| POST | `/v1/auth/refresh` | 否 | 刷新 token |
|
||||
| POST | `/v1/auth/logout` | 是 | 注销 |
|
||||
| GET | `/v1/config` | 否 | 站点配置 |
|
||||
| GET | `/v1/banners` | 否 | 首页轮播 |
|
||||
| GET | `/v1/home/courses` | 否 | 首页新课列表 |
|
||||
| GET | `/v1/home/free-courses` | 否 | 首页免费试听入口 |
|
||||
| GET | `/v1/articles` | 否 | 知识速通列表(分页) |
|
||||
| GET | `/v1/articles/:id` | 否 | 文章详情 |
|
||||
| GET | `/v1/tips` | 否 | 课程页文案 |
|
||||
| GET | `/v1/courses` | 可选 | 课程列表(含未购买,用于购买入口) |
|
||||
| GET | `/v1/courses/:code` | 可选 | 课程详情 |
|
||||
| GET | `/v1/courses/:code/lessons` | 是 | 课时列表,**校验权益** |
|
||||
| GET | `/v1/courses/:code/play` | 是 | 签发播放地址,**校验权益 + URL 签名** |
|
||||
| GET | `/v1/free-lessons` | 否 | 免费试听课时 |
|
||||
| GET | `/v1/me` | 是 | 个人资料 |
|
||||
| PATCH | `/v1/me` | 是 | 修改昵称 / 头像 |
|
||||
| GET | `/v1/me/entitlements` | 是 | 我的课程 |
|
||||
| POST | `/v1/orders` | 是 | 下单,返回 payData |
|
||||
| GET | `/v1/orders/:outTradeNo` | 是 | 查询订单状态 |
|
||||
| POST | `/v1/payments/notify` | 签名 | 发货推送(微信调用,返回 XML) |
|
||||
|
||||
#### 管理后台 `/admin/v1/*`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| POST | `/admin/v1/auth/login` | 管理员登录 |
|
||||
| GET | `/admin/v1/stats` | 数据看板 |
|
||||
| GET/PATCH | `/admin/v1/users`、`/users/:id` | 用户管理 |
|
||||
| CRUD | `/admin/v1/courses` | 课程管理 |
|
||||
| CRUD | `/admin/v1/lessons` | 课时管理 |
|
||||
| CRUD | `/admin/v1/home-courses` | 首页新课长图 |
|
||||
| CRUD | `/admin/v1/home-free-courses` | 首页试听 |
|
||||
| CRUD | `/admin/v1/banners` | 轮播图 |
|
||||
| CRUD | `/admin/v1/articles` | 文章 |
|
||||
| GET/PUT | `/admin/v1/settings` | 系统配置 |
|
||||
| GET | `/admin/v1/orders` | 订单列表 |
|
||||
| POST | `/admin/v1/orders/:id/refund` | 发起退款(Android) |
|
||||
| GET/POST/DELETE | `/admin/v1/entitlements` | 权益发放 / 回收 |
|
||||
| POST | `/admin/v1/legacy-codes/redeem` | 旧口令码人工核销 |
|
||||
|
||||
### 5.4 统一契约
|
||||
|
||||
**响应体**
|
||||
|
||||
```json
|
||||
{ "code": 0, "message": "ok", "data": {}, "requestId": "..." }
|
||||
```
|
||||
|
||||
**错误码分段**
|
||||
|
||||
| 区间 | 含义 |
|
||||
|---|---|
|
||||
| 0 | 成功 |
|
||||
| 1000–1999 | 通用(参数错误、资源不存在) |
|
||||
| 2000–2999 | 鉴权(未登录、token 过期、无权限) |
|
||||
| 3000–3999 | 业务(无权益、课程下架) |
|
||||
| 4000–4999 | 支付(订单状态非法、签名失败) |
|
||||
| 5000+ | 服务端异常 |
|
||||
|
||||
HTTP 状态码独立正确(401 / 403 / 404 / 422 / 500)。分页统一 `?page=&pageSize=`,返回 `{ items, total, page, pageSize }`。
|
||||
|
||||
### 5.5 鉴权设计
|
||||
|
||||
```text
|
||||
小程序
|
||||
wx.login -> code
|
||||
-> POST /v1/auth/wx-login
|
||||
-> 服务端 code2Session 换 openid + sessionKey
|
||||
-> upsert users,加密存储 sessionKey
|
||||
-> 返回 accessToken(2h) + refreshToken(30d)
|
||||
|
||||
管理员
|
||||
admin_users + argon2 校验 + 独立 JWT(不同 audience)
|
||||
小程序 token 访问 /admin/v1/* 一律 403
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
1. openid **不再作为身份凭证传递**,改为 JWT。解决现网「拿到别人 openid 即可查其已购课程」的问题。
|
||||
2. sessionKey 必须服务端托管,因为虚拟支付 `signature` 依赖它。
|
||||
3. accessToken 与 refreshToken 使用不同密钥或不同 audience。
|
||||
|
||||
### 5.6 兼容层策略
|
||||
|
||||
在 `/API/*.php` 原路径上原样实现现有接口,**返回体与现网逐字节一致**。
|
||||
|
||||
**目的**:让小程序零改动即可切到新后端。风险从「全站重写」降为「换后端,先验证再切流量」。
|
||||
|
||||
**性质**:临时产物。随阶段 8 的前端改造一并删除。
|
||||
|
||||
#### 接口对照表
|
||||
|
||||
| 现有前端函数 | 现路径 | 新接口 | 备注 |
|
||||
|---|---|---|---|
|
||||
| ApiGetConfig | `/API/ApiGetConfig.php` | `GET /v1/config` | |
|
||||
| ApiGetUserOpenID | `/API/ApiGetUserOpenID_Yelay.php` | 内部化,不对外 | |
|
||||
| ApiGetUserDataFromDB | `/API/ApiGetOrInsertUserDataFromDB.php` | `GET /v1/me` | |
|
||||
| ApiGetUserCourseDataFromDB | `/API/ApiGetUserCourseDataFromDB.php` | `GET /v1/me/entitlements` | |
|
||||
| ApiGetBannerImg | `/API/ApiGetBannerImg.php` | `GET /v1/banners` | |
|
||||
| ApiGetIndexCourseList | `/API/ApiGetIndexCourseList.php` | `GET /v1/home/courses` | |
|
||||
| ApiGetIndexFreeCourseList | `/API/ApiGetIndexFreeCourseList.php` | `GET /v1/home/free-courses` | |
|
||||
| ApiGetWebPagesFromDB | `/API/ApiGetWebPagesFromDB.php` | `GET /v1/articles` | 顺带修掉 varchar 排序 bug |
|
||||
| ApiGetWebPageContent | 已弃用 | 不实现 | |
|
||||
| ApiGetFreeCourseData | `/API/ApiGetFreeCourseData.php` | `GET /v1/free-lessons` | |
|
||||
| ApiGetCourseData | `/API/ApiGetCourseData.php` | `GET /v1/courses/:code/lessons` | 鉴权改为查 entitlements |
|
||||
| ApiLogin | `/API/ApiLogin.php` | `POST /v1/auth/wx-login` | 语义变化 |
|
||||
| ApiLogout | `/API/ApiLogout.php` | `POST /v1/auth/logout` | |
|
||||
| ApiChangeInfo | `/API/ApiChangeInfo.php` | `PATCH /v1/me` | |
|
||||
| ApiGetTips | `/API/ApiGetTips.php` | `GET /v1/tips` | |
|
||||
| ApiPasswordCodeValidate | `/API/ApiPasswordCodeValidate.php` | 废弃 | |
|
||||
| ApiGetPasswordCode | `/API/ApiGetPasswordCode.php` | 废弃 | |
|
||||
| ApiAddPasswordCode | `/API/ApiAddPasswordCode.php` | 废弃 | |
|
||||
| ApiAddPasswordCodeBefor | `/API/ApiAddPasswordCodeBefor.php` | 废弃 | |
|
||||
| ApiChangePasswordCode | `/API/ApiChangePasswordCode.php` | 废弃 | |
|
||||
| ApiDeletePasswordCode | `/API/ApiDeletePasswordCode.php` | 废弃 | |
|
||||
|
||||
#### 服务端直接删除的文件
|
||||
|
||||
`SMS/`(整套)、`mail.php`、`ApiGetUUID.php`、`ApiGetShortUUID.php`、`ApiGetContact.php`、`ApiChangeUserInfo.php`、`ApiChangeUserInfo_yelay.php`、`ApiGetUserOpenID.php`(旧 AppID)、`已弃用ApiGetWebPageContent.php`、`ApiGetCourseID.php`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 支付设计
|
||||
|
||||
### 6.1 流程
|
||||
|
||||
```text
|
||||
用户点「购买」
|
||||
|
|
||||
+- (1) POST /v1/orders { courseCode }
|
||||
| 服务端生成唯一 out_trade_no,订单落库 status=created
|
||||
| 返回 signData + paySig + signature
|
||||
|
|
||||
+- (2) 前端 wx.requestVirtualPayment({ mode: 'short_series_goods', ... })
|
||||
|
|
||||
+- (3) 支付成功后,微信推送发货通知
|
||||
| POST /v1/payments/notify(XML)
|
||||
| 按 wx_order_id 幂等 -> 写 entitlements -> order.status=delivered
|
||||
| 返回 <xml><ErrCode>0</ErrCode>...</xml>
|
||||
|
|
||||
+- (4) 推送丢失时,@nestjs/schedule 每 5 分钟 query_order 补发
|
||||
```
|
||||
|
||||
### 6.2 必须遵守的纪律
|
||||
|
||||
1. **发货只认「发货推送」和「查单」**。前端支付 `success` 回调不作为发货依据——用户中途退出会丢失回调。
|
||||
2. **幂等键使用平台单号 `wx_order_id`**,不是自己的 `out_trade_no`。
|
||||
3. 签名 `post_body` 必须与实际发出的字符串**逐字节一致**:不格式化、不改键顺序。
|
||||
4. 状态机严格单向:`created → paid → delivered → refunded`,禁止跳状态。
|
||||
5. 发货推送与应答消息体均为 **XML**。
|
||||
6. 查单接口参数名是 `order_id`(传 `out_trade_no`),**不是** `out_trade_no`。
|
||||
|
||||
### 6.3 签名算法
|
||||
|
||||
```text
|
||||
# paySig(服务端计算,密钥为 AppKey)
|
||||
msg = uri + '&' + post_body
|
||||
paySig = HMAC_SHA256(appkey, msg)
|
||||
|
||||
# signature(用户态签名,密钥为该用户的 sessionKey)
|
||||
signature = HMAC_SHA256(session_key, post_body)
|
||||
```
|
||||
|
||||
- C 端下单:`uri` 固定为 `requestVirtualPayment`
|
||||
- B 端接口:`uri` 为实际路径,如 `/xpay/query_order`
|
||||
|
||||
### 6.4 商品配置
|
||||
|
||||
10 门课程 = 10 个道具,需先在 MP 后台【道具管理】创建并发布,拿到道具 ID 与价格,写入 `products` 表。`signData.goodsPrice` 必须与后台价格一致(单位:分)。
|
||||
|
||||
### 6.5 费率与限制
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 个人主体月限额 | 10 万元(全终端合计) |
|
||||
| Android 结算 | T+3,费率 1% |
|
||||
| iOS 结算 | 约 45–60 天,Apple 佣金 12% |
|
||||
| iOS 最低微信版本 | 8.0.68 |
|
||||
| iOS 退款 | 用户向 App Store 申请,开发者无法主动退 |
|
||||
| Android 退款 | 开发者可主动发起 |
|
||||
|
||||
> 定价必须把 iOS 的 12% 佣金计入成本。
|
||||
|
||||
---
|
||||
|
||||
## 7. 管理后台设计
|
||||
|
||||
### 7.1 概况
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 域名 | `admin.yeomai.site` |
|
||||
| 接口 | `admin-api.yeomai.site` → 同一 Node 进程 `/admin/v1/*` |
|
||||
| 技术 | Next.js + TypeScript + shadcn/ui + TanStack Table |
|
||||
| 登录 | 独立账号密码(argon2),与微信用户体系完全隔离;可选 TOTP |
|
||||
|
||||
### 7.2 功能清单
|
||||
|
||||
1. **数据看板**:用户数、订单数、成交额、课程销量。
|
||||
2. **用户管理**:查询、改昵称头像、封禁。
|
||||
3. **课程管理**:课程 CRUD、上下架、排序、封面。
|
||||
4. **课时管理**:按课程列出课时,编辑标题/章节/排序。
|
||||
5. **权益管理**:手动发放、回收、按用户查权益(**替代原发码功能**)。
|
||||
6. **订单管理**:列表、详情、状态、发起退款(Android)。
|
||||
7. **旧口令码核销**:输入口令码 + 选择用户 → 扣减次数 → 发放权益。
|
||||
8. **内容管理**:Banner、首页新课长图、首页试听入口、文章、课程页文案。
|
||||
9. **系统配置**:`settings` 表全部键值。
|
||||
|
||||
---
|
||||
|
||||
## 8. 小程序端设计
|
||||
|
||||
### 8.1 保持不变的项
|
||||
|
||||
- 全部 `.wxml` / `.wxss` / `static/` 图片 / 图标 / 布局 / 交互手势。
|
||||
- tabBar 三项:首页、知识、我的。
|
||||
- 页面跳转关系与参数。
|
||||
- 所有对外图片与视频资源地址。
|
||||
|
||||
> 「外观不变」的实现方式:**样式与布局文件直接复用,只重写 JS 层**。这样外观天然一致,无需做像素级走查。
|
||||
|
||||
### 8.2 页面清单:11 → 10
|
||||
|
||||
| 页面 | 路径 | 处理 |
|
||||
|---|---|---|
|
||||
| 首页 | `pages/index/index` | 保留 |
|
||||
| 知识 | `pages/kecheng/kecheng` | 保留 + 扩展购买入口 |
|
||||
| 我的 | `pages/wode/wode` | 保留 |
|
||||
| 登录/退出 | `pages/LoginOrOutDetails/LoginOrOutDetails` | 保留,逻辑简化 |
|
||||
| 文章详情 | `pages/WebPageDetails/WebPageDetails` | 保留 |
|
||||
| 付费课播放 | `pages/VideoDetails/VideoDetails` | 保留 + 购买按钮 |
|
||||
| 新课长图 | `pages/IndexNewCourseDetails/IndexNewCourseDetails` | 保留 + 购买按钮 |
|
||||
| 免费试听播放 | `pages/IndexFreeCourseDetails/IndexFreeCourseDetails` | 保留 |
|
||||
| 修改资料 | `pages/ChangeUserInfo/ChangeUserInfo` | 保留 |
|
||||
| **介绍宣传页** | `pages/VideoDetailsPage/VideoDetailsPage` | **保留,实现逻辑不变** |
|
||||
| 隐藏管理台 | `pages/SetPasswordCode/SetPasswordCode` | **删除**(功能迁至管理后台) |
|
||||
|
||||
### 8.3 VideoDetailsPage 特殊说明
|
||||
|
||||
该页面是对外宣传页(初中数学公式定理资料,21 张图片,指向 `WebPages/2/1.jpg` ~ `21.jpg`)。
|
||||
|
||||
现有实现:
|
||||
|
||||
```text
|
||||
VideoDetailsPage.wxml
|
||||
1 <text>VideoDetailsPage</text>
|
||||
2 <text>富文本测试</text>
|
||||
3 <Parser html="{{htmlSnip}}" />
|
||||
```
|
||||
|
||||
正文 HTML 硬编码在 `VideoDetailsPage.js` 的 `htmlSnip` 常量中,经 Parser 组件渲染。
|
||||
|
||||
**处理方式(ADR-005)**:实现逻辑保持不变——不迁入数据库、不改由后台维护。Parser 组件替换为 npm 维护版后,该页对外行为完全一致。
|
||||
|
||||
> ⚠️ **该页路径必须原样保留。** 代码中虽然没有跳转引用,但 `sitemap.json` 为全量允许索引,且可通过小程序码、分享、搜一搜直达。路径变更会导致已分发的入口失效。
|
||||
|
||||
### 8.4 需要重写的部分
|
||||
|
||||
1. **请求层**:新增统一 `request` 封装,自动附带 token、统一错误提示、401 自动重新登录。替代现有 21 个函数各自手写 `wx.request` + Promise 的写法。
|
||||
2. **状态层**:新增极简状态模块,替代散落在各页面的 `wx.getStorageSync('UserInfoCache')`。
|
||||
3. **Parser**:自研 `Parser/` 组件替换为 npm 维护版 `mp-html`(对外行为一致,`VideoDetailsPage` 与文章页不受影响)。
|
||||
4. **移除轮询**:删除 `kecheng.js` 中 10ms × 500 次的登录等待轮询,改为 Promise 完成后驱动。
|
||||
5. **修复首启竞态**:`index.js` / `wode.js` 在 `data` 中同步读取异步写入的 `Config` 缓存,首次安装必然报错。
|
||||
6. **清理**:删除 `LoginOrOutDetails.js` 中大段注释掉的旧上传代码。
|
||||
|
||||
### 8.5 购买入口(唯一新增 UI)
|
||||
|
||||
> ⚠️ 加入支付后需要新增购买入口,这是「外观完全不变」的**唯一例外**——现有系统没有任何商品列表或购买按钮,付费课全靠口令码兑换。
|
||||
|
||||
采用最小侵入方案,复用现有视觉语言:
|
||||
|
||||
1. **首页长图页**(`IndexNewCourseDetails`)底部增加「立即购买」按钮,对应 `home_courses.course_id`。
|
||||
2. **知识页**(`kecheng`)在有课时把未购课程一并列出(锁定态),点入课程详情页购买。
|
||||
|
||||
不新增 tab、不新增页面类型。
|
||||
|
||||
---
|
||||
|
||||
## 9. 数据迁移方案
|
||||
|
||||
### 9.1 迁移前体检结论
|
||||
|
||||
对生产库做过完整性校验,结果如下:
|
||||
|
||||
| 检查项 | 结果 |
|
||||
|---|---|
|
||||
| 课程目录数 | 10 门 |
|
||||
| 有课时的课程 | 10 门 |
|
||||
| 被购买过的课程 | 10 门 |
|
||||
| 购买记录指向但目录中不存在的课程 | 无 |
|
||||
| 有课时但目录中不存在的课程 | 无 |
|
||||
| 目录中有但无课时的课程 | 无 |
|
||||
|
||||
**结论:零孤儿数据,映射关系完整,迁移无需人工补救。**
|
||||
|
||||
### 9.2 迁移映射
|
||||
|
||||
| 源 | 目标 | 关键处理 |
|
||||
|---|---|---|
|
||||
| `用户信息` | `users` | 字段改名;时间字段转 UTC |
|
||||
| `用户已购课程` | `entitlements` | `source = legacy`,`granted_at = 购课日期`;48 条 |
|
||||
| `课程口令码读取信息` | `courses` | `课程id → code`(原值保留) |
|
||||
| `视频课程数据` | `lessons` | `章节id` / `播放id` 原值保留;841 条 |
|
||||
| `免费视频课程数据` | `free_lessons` | |
|
||||
| `首页课程列表` | `home_courses` | `课程名称` 原值保留(图片路径依赖) |
|
||||
| `首页免费课程列表` | `home_free_courses` | 同上 |
|
||||
| `首页横幅图片` | `banners` | `图片名` 原值保留 |
|
||||
| `网页列表` | `articles` | `网页id → legacy_page_id`(图片路径依赖) |
|
||||
| `课程页面Tips` | `course_tips` | |
|
||||
| `cloudconfig` | `settings` | 数字 ID → 字符串 key,见 9.3 |
|
||||
| `课程口令码` | `legacy_redeem_codes` | 原样保留,见 9.4 |
|
||||
|
||||
### 9.3 settings 键映射
|
||||
|
||||
| 原 ID | 原 value | 新 key | 说明 |
|
||||
|---|---|---|---|
|
||||
| 0 | 占空 | — | 废弃 |
|
||||
| 1 | V0.0.9 | `site.version` | 版本 |
|
||||
| 2 | 付费课程 | `home.title.paid` | 标题 |
|
||||
| 3 | 试听课程 | `home.title.trial` | 标题 |
|
||||
| 4 | 公开课程 | `home.title.public` | 标题 |
|
||||
| 5 | Yelaymaths | `profile.qrcode_image` | 二维码图片名 |
|
||||
| 6 | 50 | `profile.headicon_count` | 头像数量 |
|
||||
| 7 | 修改资料 | `profile.menu.change_info` | 我的页面 |
|
||||
| 8 | 好友分享 | `profile.menu.share` | 我的页面 |
|
||||
| 9 | 在线沟通 | `profile.menu.contact` | 我的页面 |
|
||||
| 10 | 添加微信:叶雷讲数学 | `profile.menu.wechat` | 我的页面 |
|
||||
| 11 | 我是有底线的! | `home.footer_text` | 首页触底提示 |
|
||||
| 12 | 长按识别二维码 | `profile.qrcode_caption` | 图片标题 |
|
||||
|
||||
### 9.4 旧口令码处理(待确认)
|
||||
|
||||
**现状**:`课程口令码` 表中有 5 条记录,剩余有效次数合计 18 次。
|
||||
|
||||
| 口令码 | 课程 | 剩余次数 |
|
||||
|---|---|---|
|
||||
| 202601 | 7年级数学同步视频课(北师大) | 3 |
|
||||
| 202602 | 8年级数学同步视频课(北师大) | 3 |
|
||||
| 202603 | 9年级数学同步视频课(北师大) | 3 |
|
||||
| 202604 | 梁老师中考数学一轮满分冲刺 | 5 |
|
||||
| 202605 | 梁老师中考数学二轮满分冲刺 | 4 |
|
||||
|
||||
**技术事实**:口令码是「次数池」,**表结构中不含用户字段**,即无法从数据推导出这 18 次分别属于谁。因此**无法自动转换成「某用户已购买某课程」的记录**——缺少用户维度。
|
||||
|
||||
**本方案的处理(无损、可兜底)**:
|
||||
|
||||
1. 5 条记录原样迁入 `legacy_redeem_codes`,作为凭证留存。
|
||||
2. 管理后台提供「旧口令码核销」功能:输入口令码 + 选择用户 → 扣减 `remaining_uses` → 写入 `entitlements(source = legacy_code)`。
|
||||
3. 当持有旧码的用户来访时,运营在后台一键发放权益,业务闭环完整。
|
||||
|
||||
> ❓ **需要确认**:若本意是「这 18 次已经卖给具体学生、且手上有对应名单」,应**直接按名单批量生成** `entitlements(source = legacy)` 记录,而不是走核销流程。请提供名单或确认按上述核销方案执行。
|
||||
|
||||
### 9.5 迁移步骤
|
||||
|
||||
1. 全量备份生产库与 `Images/`、`WebPages/`、`Course/`、`FreeVideos/` 目录。
|
||||
2. 建立新库与数据库账号。
|
||||
3. 执行 `prisma migrate deploy` 建表。
|
||||
4. 执行迁移脚本(`tools/migrate-legacy/`),要求:**幂等、可重复执行、逐步计数校验**。
|
||||
5. 数据校验:
|
||||
- 逐表行数比对。
|
||||
- 随机抽取 10 个用户,比对迁移前后「已购课程集合」是否完全一致。
|
||||
- 校验每个 `entitlement.course_id` 在 `courses` 中均存在。
|
||||
- 校验 `courses.code` 与源数据逐字节一致。
|
||||
6. 在 staging(`api_yeomai_site_develop`)跑通全部接口。
|
||||
7. 生产切换,**旧库保留只读**作为回滚路径。
|
||||
|
||||
### 9.6 回滚方案
|
||||
|
||||
- 切换期 PHP 服务与 Node 服务在同一台机器共存。
|
||||
- 小程序端在兼容层阶段未改动,回滚只需切换域名解析。
|
||||
- 数据库切换前做全量快照,出问题直接回退。
|
||||
|
||||
---
|
||||
|
||||
## 10. 实施路线图
|
||||
|
||||
| 阶段 | 内容 | 工期 | 交付物 |
|
||||
|---|---|---|---|
|
||||
| 0 准备 | 宝塔 Node 环境试通(**最大未知项**);备份;建 monorepo;确认 MySQL 账号权限 | 1–2 天 | 可运行的部署链路 |
|
||||
| 1 数据端 | Prisma schema + 迁移 + 数据字典 + 迁移脚本 + 校验 | 3–4 天 | 新库可用,数据完整 |
|
||||
| 2 服务端骨架 | NestJS + Prisma + zod + pino + Swagger;统一响应/异常/守卫;鉴权 + sessionKey 托管 | 4–5 天 | `/v1` 核心接口可用 |
|
||||
| 3 兼容层 | `/API/*.php` 原样实现 + 逐接口对拍测试;staging 用小体验证 | 5–7 天 | 小程序可切新后端 |
|
||||
| 4 生产切换 | 切域名,保留回滚路径 | 1 天 | 生产运行在 Node 上 |
|
||||
| 5 管理后台 | Next.js 后台全功能;删除小程序隐藏管理页 | 1–2 周 | 运营可脱离 SQL |
|
||||
| 6 支付 | 开通虚拟支付 → 道具 → 下单/推送/幂等/兜底 → 订单状态机 | 1–2 周 | 可自助购买 |
|
||||
| 7 视频防盗链 | 对象存储 + CDN 签名 + `/play` 接口 | 3–5 天 | 付费内容不可直链 |
|
||||
| 8 前端规范化 | request 封装、状态模块、去轮询、换 Parser、新增购买入口;删兼容层与死代码 | 4–5 天 | 全量切 `/v1` |
|
||||
|
||||
**总计约 6–8 周。**
|
||||
|
||||
> 顺序原则:**先让新后端原样接管(阶段 1–4),再谈支付(阶段 6)。** 支付本身不难,难的是把它塞进一个没有鉴权、没有订单表、没有后台的系统里。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险登记册
|
||||
|
||||
| 编号 | 风险 | 影响 | 应对 | 阶段 |
|
||||
|---|---|---|---|---|
|
||||
| R1 | 宝塔 Node 项目管理器对 pnpm workspace 支持不足 | 阻塞部署 | 阶段 0 先行验证;备选「CI 构建后仅部署产物」 | 0 |
|
||||
| R2 | MySQL 5.7 账号无 `CREATE DATABASE` 权限导致 Prisma shadow DB 失败 | 阻塞迁移 | 授权,或本地 `migrate diff` + 生产 `migrate deploy` | 1 |
|
||||
| R3 | `courses.code` 被改写 | 全部视频资源失效 | 迁移脚本禁止改写;加校验断言 | 1 |
|
||||
| R4 | 封面 / 图片文件名被改写 | 图片 404 | 迁移脚本原值透传;加校验断言 | 1 |
|
||||
| R5 | 时间字段未做 UTC 转换 | 订单时间偏移 8 小时 | 迁移时显式转换 + 抽样核对 | 1 |
|
||||
| R6 | 18 次旧口令码归属不明 | 用户投诉 | 保留原表 + 后台核销入口;或按名单批量发放 | 1 / 5 |
|
||||
| R7 | `VideoDetailsPage` 路径变更 | 已分发的入口失效 | 路径原样保留 | 8 |
|
||||
| R8 | `sessionKey` 泄漏 | 用户态签名可被伪造 | 加密存储;日志脱敏 | 2 |
|
||||
| R9 | 发货推送丢失 | 用户付款未开通 | 双通道:推送 + 定时查单 | 6 |
|
||||
| R10 | iOS 12% 佣金与 45–60 天结算 | 毛利与现金流 | 定价计入佣金;预留周转 | 6 |
|
||||
| R11 | 宝塔 MySQL 5.7 已 EOL | 无安全补丁 | 接受;保证每日备份可恢复 | 全部 |
|
||||
| R12 | 生产密钥曾在 git / dump 中泄露 | 数据与资金风险 | 全部轮换:DB 密码、阿里云 AK/SK、邮箱授权码、小程序 AppSecret | 0 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 待确认事项
|
||||
|
||||
| 编号 | 事项 | 现状 | 需要 |
|
||||
|---|---|---|---|
|
||||
| Q1 | 旧口令码的 18 次剩余次数 | 无用户维度,无法自动转成「用户已购买」 | 确认按 9.4 核销方案执行,或提供持有者名单 |
|
||||
| Q2 | `VideoDetailsPage` 的实际访问入口 | 代码中无跳转引用,但可能经小程序码 / 搜索直达 | 确认是否对外分发过;若通过小程序码分发,路径必须保留 |
|
||||
| Q3 | 头像资源数量 | `cloudconfig` 记录为 50,`Images/Headicons/` 实际 51 个文件 | 确认有效范围 |
|
||||
| Q4 | 三大内容块是否全保留 | 默认全部保留 | 确认无内容需要下线 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:目标仓库结构
|
||||
|
||||
```text
|
||||
yelaymaths/
|
||||
├─ apps/
|
||||
│ ├─ api/ # NestJS 服务端
|
||||
│ │ ├─ src/
|
||||
│ │ │ ├─ modules/
|
||||
│ │ │ ├─ legacy/ # 兼容层(临时)
|
||||
│ │ │ ├─ common/
|
||||
│ │ │ └─ main.ts
|
||||
│ │ ├─ prisma/
|
||||
│ │ │ ├─ schema.prisma
|
||||
│ │ │ └─ migrations/
|
||||
│ │ └─ test/
|
||||
│ ├─ admin/ # Next.js 管理后台
|
||||
│ └─ mp/ # 微信小程序(原生)
|
||||
├─ packages/
|
||||
│ ├─ shared/ # zod schema、枚举、DTO 类型
|
||||
│ └─ tsconfig/ # 共享 TS / ESLint 预设
|
||||
├─ tools/
|
||||
│ └─ migrate-legacy/ # 老库 → 新库迁移脚本
|
||||
├─ docs/
|
||||
│ └─ 重构开发文档.md
|
||||
├─ docker-compose.yml
|
||||
└─ .gitlab-ci.yml
|
||||
```
|
||||
|
||||
## 附录 B:现有 10 门付费课程
|
||||
|
||||
| course.code 前缀 | 课程名 |
|
||||
|---|---|
|
||||
| `98869f2f771e…` | 几何模型 |
|
||||
| `1e7d43480f4e…` | 中考数学压轴大招 |
|
||||
| `1da939cf5f61…` | 7年级同步课程 |
|
||||
| `0656b64f6e60…` | 8年级同步课程 |
|
||||
| `2e52fbb3c8d9…` | 9年级同步课程 |
|
||||
| `cf944197d5fb…` | 7年级数学同步视频课(北师大) |
|
||||
| `b711b037fb7b…` | 8年级数学同步视频课(北师大) |
|
||||
| `6ec33e18e8ab…` | 9年级数学同步视频课(北师大) |
|
||||
| `25caa811c67c…` | 梁老师中考数学一轮满分冲刺(视频+讲义) |
|
||||
| `77d091c27378…` | 梁老师中考数学二轮满分冲刺(视频+讲义) |
|
||||
|
||||
File renamed without changes.
+484
-484
@@ -1,485 +1,485 @@
|
||||
const baseurl="https://api.yeomai.site"
|
||||
|
||||
|
||||
//获取用户的OpenID
|
||||
function ApiGetUserOpenID() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success: res => {
|
||||
if (res.code) {
|
||||
// 发起网络请求
|
||||
wx.request({
|
||||
url: baseurl + '/API/ApiGetUserOpenID_Yelay.php',
|
||||
data: {
|
||||
code: res.code
|
||||
},
|
||||
success: res => {
|
||||
const openid = res.data;
|
||||
resolve(openid); // 将获取的openid通过Promise的resolve传递
|
||||
},
|
||||
fail: () => {
|
||||
reject(null); // 如果请求失败,传递null给Promise的reject
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('换取用户code失败!登录失败!');
|
||||
reject(null); // 如果登录失败,传递null给Promise的reject
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
console.log('获取用户openid失败!登录失败');
|
||||
reject(null); // 如果登录失败,传递null给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//使用OpenID获取数据库的用户信息,没有信息则插入新用户
|
||||
function ApiGetUserDataFromDB(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetOrInsertUserDataFromDB.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//使用OpenID获取数据库的用户课程购买信息
|
||||
function ApiGetUserCourseDataFromDB(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetUserCourseDataFromDB.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ApiGetWebPagesFromDB(page) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetWebPagesFromDB.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
page: page
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
function ApiGetWebPageContent(page) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetWebPageContent.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
page: page
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetCourseData(openid, courseid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetCourseData.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
courseid: courseid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetFreeCourseData(index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetFreeCourseData.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
index: index
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiLogout(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiLogout.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiLogin(openid,username) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiLogin.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
username: username,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiChangeInfo(openid,username,headiconID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiChangeInfo.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
username: username,
|
||||
headiconID: headiconID,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiPasswordCodeValidate(openid, passwordcode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiPasswordCodeValidate.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
PasswordCode:passwordcode
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetIndexCourseList() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetIndexCourseList.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetIndexFreeCourseList() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetIndexFreeCourseList.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetBannerImg() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetBannerImg.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetConfig.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiAddPasswordCode(openid,courseID,courseName,coursePswdCode,courseEndDate,courseInfo,courseCount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiAddPasswordCode.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
courseID: courseID,
|
||||
courseName: courseName,
|
||||
coursePswdCode: coursePswdCode,
|
||||
courseEndDate: courseEndDate,
|
||||
courseInfo: courseInfo,
|
||||
courseCount: courseCount,
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json' // 设置请求头为JSON格式
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiAddPasswordCodeBefor(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiAddPasswordCodeBefor.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
function ApiGetPasswordCode(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetPasswordCode.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiChangePasswordCode(openid,coursePswdCode,courseEndDate,courseInfo,courseCount,oldCoursePswdCode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiChangePasswordCode.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
coursePswdCode: coursePswdCode,
|
||||
courseEndDate: courseEndDate,
|
||||
courseInfo: courseInfo,
|
||||
courseCount: courseCount,
|
||||
oldCoursePswdCode: oldCoursePswdCode,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiDeletePasswordCode(openid,coursePswdCode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiDeletePasswordCode.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
coursePswdCode: coursePswdCode,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetTips() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetTips.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ApiGetUserOpenID,
|
||||
ApiGetUserDataFromDB,
|
||||
ApiGetWebPagesFromDB,
|
||||
ApiGetWebPageContent,
|
||||
ApiGetUserCourseDataFromDB,
|
||||
ApiLogin,
|
||||
ApiLogout,
|
||||
ApiChangeInfo,
|
||||
ApiGetCourseData,
|
||||
ApiGetFreeCourseData,
|
||||
ApiPasswordCodeValidate,
|
||||
ApiGetIndexCourseList,
|
||||
ApiGetIndexFreeCourseList,
|
||||
ApiGetBannerImg,
|
||||
ApiGetConfig,
|
||||
ApiAddPasswordCode,
|
||||
ApiAddPasswordCodeBefor,
|
||||
ApiGetPasswordCode,
|
||||
ApiChangePasswordCode,
|
||||
ApiDeletePasswordCode,
|
||||
ApiGetTips
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
其他页面调用方法是:
|
||||
首先引入
|
||||
const MyApi = require('../../Api/api.js');
|
||||
|
||||
然后调用,返回值就是result
|
||||
MyApi.ApiGetUserOpenID()
|
||||
.then(result => {
|
||||
// 成功后执行的代码
|
||||
console.log(result);
|
||||
})
|
||||
.catch(err => {
|
||||
// 失败后执行的代码
|
||||
console.error("请求失败:"+err);
|
||||
});
|
||||
|
||||
|
||||
|
||||
数据传递方式:
|
||||
GET请求: 数据通过URL参数传递,可以在URL中看到。
|
||||
POST请求: 数据通过请求体(body)传递,不在URL中可见。
|
||||
|
||||
数据大小限制:
|
||||
GET请求: 有长度限制,通常在几千个字符之间,因为URL有长度限制。
|
||||
POST请求: 没有固定的长度限制,可以传递大量的数据。
|
||||
|
||||
安全性:
|
||||
GET请求: 因为数据在URL中可见,不适合传递敏感信息,且容易被拦截。
|
||||
POST请求: 数据在请求体中,相对更安全,适用于敏感信息传递。
|
||||
|
||||
const baseurl="https://api.yeomai.site"
|
||||
|
||||
|
||||
//获取用户的OpenID
|
||||
function ApiGetUserOpenID() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success: res => {
|
||||
if (res.code) {
|
||||
// 发起网络请求
|
||||
wx.request({
|
||||
url: baseurl + '/API/ApiGetUserOpenID_Yelay.php',
|
||||
data: {
|
||||
code: res.code
|
||||
},
|
||||
success: res => {
|
||||
const openid = res.data;
|
||||
resolve(openid); // 将获取的openid通过Promise的resolve传递
|
||||
},
|
||||
fail: () => {
|
||||
reject(null); // 如果请求失败,传递null给Promise的reject
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('换取用户code失败!登录失败!');
|
||||
reject(null); // 如果登录失败,传递null给Promise的reject
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
console.log('获取用户openid失败!登录失败');
|
||||
reject(null); // 如果登录失败,传递null给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//使用OpenID获取数据库的用户信息,没有信息则插入新用户
|
||||
function ApiGetUserDataFromDB(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetOrInsertUserDataFromDB.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//使用OpenID获取数据库的用户课程购买信息
|
||||
function ApiGetUserCourseDataFromDB(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetUserCourseDataFromDB.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ApiGetWebPagesFromDB(page) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetWebPagesFromDB.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
page: page
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
function ApiGetWebPageContent(page) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetWebPageContent.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
page: page
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetCourseData(openid, courseid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetCourseData.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
courseid: courseid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetFreeCourseData(index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetFreeCourseData.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
index: index
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiLogout(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiLogout.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiLogin(openid,username) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiLogin.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
username: username,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiChangeInfo(openid,username,headiconID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiChangeInfo.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
username: username,
|
||||
headiconID: headiconID,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiPasswordCodeValidate(openid, passwordcode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiPasswordCodeValidate.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
PasswordCode:passwordcode
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetIndexCourseList() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetIndexCourseList.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetIndexFreeCourseList() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetIndexFreeCourseList.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetBannerImg() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetBannerImg.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetConfig.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiAddPasswordCode(openid,courseID,courseName,coursePswdCode,courseEndDate,courseInfo,courseCount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiAddPasswordCode.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
courseID: courseID,
|
||||
courseName: courseName,
|
||||
coursePswdCode: coursePswdCode,
|
||||
courseEndDate: courseEndDate,
|
||||
courseInfo: courseInfo,
|
||||
courseCount: courseCount,
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json' // 设置请求头为JSON格式
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiAddPasswordCodeBefor(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiAddPasswordCodeBefor.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
function ApiGetPasswordCode(openid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetPasswordCode.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiChangePasswordCode(openid,coursePswdCode,courseEndDate,courseInfo,courseCount,oldCoursePswdCode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiChangePasswordCode.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
coursePswdCode: coursePswdCode,
|
||||
courseEndDate: courseEndDate,
|
||||
courseInfo: courseInfo,
|
||||
courseCount: courseCount,
|
||||
oldCoursePswdCode: oldCoursePswdCode,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiDeletePasswordCode(openid,coursePswdCode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiDeletePasswordCode.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
data: {
|
||||
openid: openid,
|
||||
coursePswdCode: coursePswdCode,
|
||||
},
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ApiGetTips() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseurl+'/API/ApiGetTips.php',
|
||||
method: 'GET', // 或 'POST'
|
||||
success: res => {
|
||||
resolve(res.data); // 将获取的结果通过Promise的resolve传递
|
||||
},
|
||||
fail: err => {
|
||||
reject(err); // 如果请求失败,传递错误给Promise的reject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ApiGetUserOpenID,
|
||||
ApiGetUserDataFromDB,
|
||||
ApiGetWebPagesFromDB,
|
||||
ApiGetWebPageContent,
|
||||
ApiGetUserCourseDataFromDB,
|
||||
ApiLogin,
|
||||
ApiLogout,
|
||||
ApiChangeInfo,
|
||||
ApiGetCourseData,
|
||||
ApiGetFreeCourseData,
|
||||
ApiPasswordCodeValidate,
|
||||
ApiGetIndexCourseList,
|
||||
ApiGetIndexFreeCourseList,
|
||||
ApiGetBannerImg,
|
||||
ApiGetConfig,
|
||||
ApiAddPasswordCode,
|
||||
ApiAddPasswordCodeBefor,
|
||||
ApiGetPasswordCode,
|
||||
ApiChangePasswordCode,
|
||||
ApiDeletePasswordCode,
|
||||
ApiGetTips
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
其他页面调用方法是:
|
||||
首先引入
|
||||
const MyApi = require('../../Api/api.js');
|
||||
|
||||
然后调用,返回值就是result
|
||||
MyApi.ApiGetUserOpenID()
|
||||
.then(result => {
|
||||
// 成功后执行的代码
|
||||
console.log(result);
|
||||
})
|
||||
.catch(err => {
|
||||
// 失败后执行的代码
|
||||
console.error("请求失败:"+err);
|
||||
});
|
||||
|
||||
|
||||
|
||||
数据传递方式:
|
||||
GET请求: 数据通过URL参数传递,可以在URL中看到。
|
||||
POST请求: 数据通过请求体(body)传递,不在URL中可见。
|
||||
|
||||
数据大小限制:
|
||||
GET请求: 有长度限制,通常在几千个字符之间,因为URL有长度限制。
|
||||
POST请求: 没有固定的长度限制,可以传递大量的数据。
|
||||
|
||||
安全性:
|
||||
GET请求: 因为数据在URL中可见,不适合传递敏感信息,且容易被拦截。
|
||||
POST请求: 数据在请求体中,相对更安全,适用于敏感信息传递。
|
||||
|
||||
*/
|
||||
@@ -1,39 +1,39 @@
|
||||
//本文件用于放置常用函数
|
||||
|
||||
function GetNowData() {
|
||||
// 获取当前时间
|
||||
var currentTime = new Date();
|
||||
// 分别获取年、月、日、时、分、秒
|
||||
var year = currentTime.getFullYear();
|
||||
var month = padZero(currentTime.getMonth() + 1); // 月份是从0开始的,所以要加1
|
||||
var day = padZero(currentTime.getDate());
|
||||
var hours = padZero(currentTime.getHours());
|
||||
var minutes = padZero(currentTime.getMinutes());
|
||||
var seconds = padZero(currentTime.getSeconds());
|
||||
// 打印输出当前时间
|
||||
|
||||
return ("当前时间:" + year + "-" + month + "-" + day + "-" + hours + "-" + minutes + "-" + seconds);
|
||||
}
|
||||
// 补零函数,不用导出
|
||||
function padZero(num) {
|
||||
return num < 10 ? '0' + num : num;
|
||||
}
|
||||
|
||||
function multiply(x, y) {
|
||||
return x * y;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GetNowData,
|
||||
multiply
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
其他页面调用方法是:
|
||||
首先引入
|
||||
const common = require('../../api/common.js');
|
||||
|
||||
然后调用,返回值就是result
|
||||
common.GetNowData();
|
||||
//本文件用于放置常用函数
|
||||
|
||||
function GetNowData() {
|
||||
// 获取当前时间
|
||||
var currentTime = new Date();
|
||||
// 分别获取年、月、日、时、分、秒
|
||||
var year = currentTime.getFullYear();
|
||||
var month = padZero(currentTime.getMonth() + 1); // 月份是从0开始的,所以要加1
|
||||
var day = padZero(currentTime.getDate());
|
||||
var hours = padZero(currentTime.getHours());
|
||||
var minutes = padZero(currentTime.getMinutes());
|
||||
var seconds = padZero(currentTime.getSeconds());
|
||||
// 打印输出当前时间
|
||||
|
||||
return ("当前时间:" + year + "-" + month + "-" + day + "-" + hours + "-" + minutes + "-" + seconds);
|
||||
}
|
||||
// 补零函数,不用导出
|
||||
function padZero(num) {
|
||||
return num < 10 ? '0' + num : num;
|
||||
}
|
||||
|
||||
function multiply(x, y) {
|
||||
return x * y;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GetNowData,
|
||||
multiply
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
其他页面调用方法是:
|
||||
首先引入
|
||||
const common = require('../../api/common.js');
|
||||
|
||||
然后调用,返回值就是result
|
||||
common.GetNowData();
|
||||
*/
|
||||
@@ -1,114 +1,114 @@
|
||||
//CssTokenizer.js
|
||||
function CssTokenizer(style = '', tagStyle = {}) {
|
||||
this.res = tagStyle;
|
||||
this._state = "SPACE";
|
||||
this._buffer = style;
|
||||
this._sectionStart = 0;
|
||||
this._index = 0;
|
||||
this._name = '';
|
||||
this._content = '';
|
||||
this._list = [];
|
||||
this._comma = false;
|
||||
}
|
||||
CssTokenizer.prototype.SPACE = function(c) {
|
||||
if (/[a-zA-Z.#]/.test(c)) {
|
||||
this._sectionStart = this._index;
|
||||
this._state = "InName";
|
||||
} else if (c == '@') this._state = "Ignore1";
|
||||
else if (c == '/') this._state = "BeforeComment";
|
||||
};
|
||||
CssTokenizer.prototype.BeforeComment = function(c) {
|
||||
if (c == '*') this._state = "InComment";
|
||||
else {
|
||||
this._index--;
|
||||
this._state = "SPACE";
|
||||
}
|
||||
};
|
||||
CssTokenizer.prototype.InComment = function(c) {
|
||||
if (c == '*') this._state = "AfterComment";
|
||||
};
|
||||
CssTokenizer.prototype.AfterComment = function(c) {
|
||||
if (c == '/') this._state = "SPACE";
|
||||
else {
|
||||
this._index--;
|
||||
this._state = "InComment"
|
||||
}
|
||||
};
|
||||
CssTokenizer.prototype.InName = function(c) {
|
||||
if (c == '{') {
|
||||
this._list.push(this._buffer.substring(this._sectionStart, this._index))
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InContent";
|
||||
} else if (c == ',') {
|
||||
this._list.push(this._buffer.substring(this._sectionStart, this._index));
|
||||
this._sectionStart = this._index + 1;
|
||||
this._comma = true;
|
||||
} else if ((c == '.' || c == '#') && !this._comma) {
|
||||
this._buffer = this._buffer.splice(this._index, 1, ' ');
|
||||
} else if (/\s/.test(c)) {
|
||||
this._name = this._buffer.substring(this._sectionStart, this._index);
|
||||
this._state = "NameSpace";
|
||||
} else if (/[>:\[]/.test(c)) {
|
||||
if (this._list.length) this._state = "IgnoreName";
|
||||
else this._state = "Ignore1";
|
||||
} else this._comma = false;
|
||||
};
|
||||
CssTokenizer.prototype.NameSpace = function(c) {
|
||||
if (c == '{') {
|
||||
this._list.push(this._name);
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InContent";
|
||||
} else if (c == ',') {
|
||||
this._comma = true;
|
||||
this._list.push(this._name);
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InName"
|
||||
} else if (/\S/.test(c)) {
|
||||
if (this._comma) {
|
||||
this._sectionStart = this._index;
|
||||
this._index--;
|
||||
this._state = "InName";
|
||||
} else if (this._list.length) this._state = "IgnoreName";
|
||||
else this._state = "Ignore1"
|
||||
}
|
||||
};
|
||||
CssTokenizer.prototype.InContent = function(c) {
|
||||
if (c == '}') {
|
||||
this._content = this._buffer.substring(this._sectionStart, this._index);
|
||||
for (let item of this._list)
|
||||
this.res[item] = (this.res[item] || '') + this._content;
|
||||
this._list = [];
|
||||
this._comma = false;
|
||||
this._state = "SPACE";
|
||||
}
|
||||
};
|
||||
CssTokenizer.prototype.IgnoreName = function(c) {
|
||||
if (c == ',') {
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InName";
|
||||
} else if (c == '{') {
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InContent";
|
||||
}
|
||||
}
|
||||
CssTokenizer.prototype.Ignore1 = function(c) {
|
||||
if (c == ';') {
|
||||
this._state = "SPACE";
|
||||
this._sectionStart = this._index + 1;
|
||||
} else if (c == '{') this._state = "Ignore2";
|
||||
};
|
||||
CssTokenizer.prototype.Ignore2 = function(c) {
|
||||
if (c == '}') {
|
||||
this._state = "SPACE";
|
||||
this._sectionStart = this._index + 1;
|
||||
} else if (c == '{') this._state = "Ignore3";
|
||||
};
|
||||
CssTokenizer.prototype.Ignore3 = function(c) {
|
||||
if (c == '}') this._state = "Ignore2";
|
||||
};
|
||||
CssTokenizer.prototype.parse = function() {
|
||||
for (; this._index < this._buffer.length; this._index++)
|
||||
this[this._state](this._buffer[this._index]);
|
||||
return this.res;
|
||||
};
|
||||
//CssTokenizer.js
|
||||
function CssTokenizer(style = '', tagStyle = {}) {
|
||||
this.res = tagStyle;
|
||||
this._state = "SPACE";
|
||||
this._buffer = style;
|
||||
this._sectionStart = 0;
|
||||
this._index = 0;
|
||||
this._name = '';
|
||||
this._content = '';
|
||||
this._list = [];
|
||||
this._comma = false;
|
||||
}
|
||||
CssTokenizer.prototype.SPACE = function(c) {
|
||||
if (/[a-zA-Z.#]/.test(c)) {
|
||||
this._sectionStart = this._index;
|
||||
this._state = "InName";
|
||||
} else if (c == '@') this._state = "Ignore1";
|
||||
else if (c == '/') this._state = "BeforeComment";
|
||||
};
|
||||
CssTokenizer.prototype.BeforeComment = function(c) {
|
||||
if (c == '*') this._state = "InComment";
|
||||
else {
|
||||
this._index--;
|
||||
this._state = "SPACE";
|
||||
}
|
||||
};
|
||||
CssTokenizer.prototype.InComment = function(c) {
|
||||
if (c == '*') this._state = "AfterComment";
|
||||
};
|
||||
CssTokenizer.prototype.AfterComment = function(c) {
|
||||
if (c == '/') this._state = "SPACE";
|
||||
else {
|
||||
this._index--;
|
||||
this._state = "InComment"
|
||||
}
|
||||
};
|
||||
CssTokenizer.prototype.InName = function(c) {
|
||||
if (c == '{') {
|
||||
this._list.push(this._buffer.substring(this._sectionStart, this._index))
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InContent";
|
||||
} else if (c == ',') {
|
||||
this._list.push(this._buffer.substring(this._sectionStart, this._index));
|
||||
this._sectionStart = this._index + 1;
|
||||
this._comma = true;
|
||||
} else if ((c == '.' || c == '#') && !this._comma) {
|
||||
this._buffer = this._buffer.splice(this._index, 1, ' ');
|
||||
} else if (/\s/.test(c)) {
|
||||
this._name = this._buffer.substring(this._sectionStart, this._index);
|
||||
this._state = "NameSpace";
|
||||
} else if (/[>:\[]/.test(c)) {
|
||||
if (this._list.length) this._state = "IgnoreName";
|
||||
else this._state = "Ignore1";
|
||||
} else this._comma = false;
|
||||
};
|
||||
CssTokenizer.prototype.NameSpace = function(c) {
|
||||
if (c == '{') {
|
||||
this._list.push(this._name);
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InContent";
|
||||
} else if (c == ',') {
|
||||
this._comma = true;
|
||||
this._list.push(this._name);
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InName"
|
||||
} else if (/\S/.test(c)) {
|
||||
if (this._comma) {
|
||||
this._sectionStart = this._index;
|
||||
this._index--;
|
||||
this._state = "InName";
|
||||
} else if (this._list.length) this._state = "IgnoreName";
|
||||
else this._state = "Ignore1"
|
||||
}
|
||||
};
|
||||
CssTokenizer.prototype.InContent = function(c) {
|
||||
if (c == '}') {
|
||||
this._content = this._buffer.substring(this._sectionStart, this._index);
|
||||
for (let item of this._list)
|
||||
this.res[item] = (this.res[item] || '') + this._content;
|
||||
this._list = [];
|
||||
this._comma = false;
|
||||
this._state = "SPACE";
|
||||
}
|
||||
};
|
||||
CssTokenizer.prototype.IgnoreName = function(c) {
|
||||
if (c == ',') {
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InName";
|
||||
} else if (c == '{') {
|
||||
this._sectionStart = this._index + 1;
|
||||
this._state = "InContent";
|
||||
}
|
||||
}
|
||||
CssTokenizer.prototype.Ignore1 = function(c) {
|
||||
if (c == ';') {
|
||||
this._state = "SPACE";
|
||||
this._sectionStart = this._index + 1;
|
||||
} else if (c == '{') this._state = "Ignore2";
|
||||
};
|
||||
CssTokenizer.prototype.Ignore2 = function(c) {
|
||||
if (c == '}') {
|
||||
this._state = "SPACE";
|
||||
this._sectionStart = this._index + 1;
|
||||
} else if (c == '{') this._state = "Ignore3";
|
||||
};
|
||||
CssTokenizer.prototype.Ignore3 = function(c) {
|
||||
if (c == '}') this._state = "Ignore2";
|
||||
};
|
||||
CssTokenizer.prototype.parse = function() {
|
||||
for (; this._index < this._buffer.length; this._index++)
|
||||
this[this._state](this._buffer[this._index]);
|
||||
return this.res;
|
||||
};
|
||||
module.exports = CssTokenizer;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,37 +1,37 @@
|
||||
String.prototype.splice = function(start = 0, deleteCount = 0, addStr = '') {
|
||||
if (start < 0) start = this.length + start;
|
||||
if (deleteCount < 0) deleteCount = 0;
|
||||
return this.substring(0, start) + addStr + this.substring(start + deleteCount);
|
||||
}
|
||||
module.exports = {
|
||||
versionHigherThan(version = '') {
|
||||
//旧版 var v1 = wx.getSystemInfoSync().SDKVersion.split('.');
|
||||
var v1 = wx.getAppBaseInfo().SDKVersion.split('.');
|
||||
var v2 = version.split('.');
|
||||
const len = Math.max(v1.length, v2.length);
|
||||
while (v1.length < len) {
|
||||
v1.push('0');
|
||||
}
|
||||
while (v2.length < len) {
|
||||
v2.push('0');
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
const num1 = parseInt(v1[i]);
|
||||
const num2 = parseInt(v2[i]);
|
||||
if (num1 > num2) {
|
||||
return true;
|
||||
} else if (num1 < num2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
html2nodes(html, tagStyle) {
|
||||
const Parser = require('./Parser.js');
|
||||
return Parser(html, tagStyle);
|
||||
},
|
||||
css2object(style, tagStyle) {
|
||||
const CssTokenizer = require('./CssTokenizer.js');
|
||||
return new CssTokenizer(style, tagStyle).parse();
|
||||
}
|
||||
String.prototype.splice = function(start = 0, deleteCount = 0, addStr = '') {
|
||||
if (start < 0) start = this.length + start;
|
||||
if (deleteCount < 0) deleteCount = 0;
|
||||
return this.substring(0, start) + addStr + this.substring(start + deleteCount);
|
||||
}
|
||||
module.exports = {
|
||||
versionHigherThan(version = '') {
|
||||
//旧版 var v1 = wx.getSystemInfoSync().SDKVersion.split('.');
|
||||
var v1 = wx.getAppBaseInfo().SDKVersion.split('.');
|
||||
var v2 = version.split('.');
|
||||
const len = Math.max(v1.length, v2.length);
|
||||
while (v1.length < len) {
|
||||
v1.push('0');
|
||||
}
|
||||
while (v2.length < len) {
|
||||
v2.push('0');
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
const num1 = parseInt(v1[i]);
|
||||
const num2 = parseInt(v2[i]);
|
||||
if (num1 > num2) {
|
||||
return true;
|
||||
} else if (num1 < num2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
html2nodes(html, tagStyle) {
|
||||
const Parser = require('./Parser.js');
|
||||
return Parser(html, tagStyle);
|
||||
},
|
||||
css2object(style, tagStyle) {
|
||||
const CssTokenizer = require('./CssTokenizer.js');
|
||||
return new CssTokenizer(style, tagStyle).parse();
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
<!--Parser-->
|
||||
<slot wx:if="{{!(html.nodes||((html[0].name||html[0].type)?1:nodes.length))}}" animation="{{hideAnimation}}"></slot>
|
||||
<trees id="contain" style="{{(showWithAnimation?'opacity:0;':'')+(selectable?'user-select:text;-webkit-user-select:text':'')}}" animation="{{showAnimation}}" nodes="{{html.nodes||((html[0].name||html[0].type)?html:nodes)}}" controls="{{controls}}" catchpreview="_previewImg"
|
||||
<!--Parser-->
|
||||
<slot wx:if="{{!(html.nodes||((html[0].name||html[0].type)?1:nodes.length))}}" animation="{{hideAnimation}}"></slot>
|
||||
<trees id="contain" style="{{(showWithAnimation?'opacity:0;':'')+(selectable?'user-select:text;-webkit-user-select:text':'')}}" animation="{{showAnimation}}" nodes="{{html.nodes||((html[0].name||html[0].type)?html:nodes)}}" controls="{{controls}}" catchpreview="_previewImg"
|
||||
catchplay="_playVideo" catchlinkpress="tapEvent" catcherror="errorEvent" />
|
||||
@@ -1,5 +1,5 @@
|
||||
:host {
|
||||
display: block;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
:host {
|
||||
display: block;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
// Parser/trees/cssHandler.wxs
|
||||
module.exports = {
|
||||
getStyle: function(style, display) {
|
||||
var res = "";
|
||||
var reg = getRegExp("float\s*:\s*[^;]*", "i");
|
||||
if (reg.test(style)) res += reg.exec(style)[0];
|
||||
reg = getRegExp("margin[^;]*auto", "i");
|
||||
if (reg.test(style)) res += (";" + reg.exec(style)[0]);
|
||||
reg = getRegExp("display\s*:\s*([^;]*)", "i");
|
||||
if (reg.test(style) && reg.exec(style)[1]!="flex") res += (';' + reg.exec(style)[0]);
|
||||
else res += (';display:' + display);
|
||||
reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig");
|
||||
var width = reg.exec(style);
|
||||
while (width) {
|
||||
res += (';' + width[0]);
|
||||
width = reg.exec(style);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
setImgStyle: function(item, imgMode) {
|
||||
if (getRegExp("[^-]width\s*:\s*[^;]*", "i").test(';' + item.attrs.style))
|
||||
item.attrs.style += ';width:100%';
|
||||
if (imgMode == "widthFix")
|
||||
item.attrs.style += ";height:auto !important";
|
||||
return [item];
|
||||
},
|
||||
setStyle: function(item) {
|
||||
if (getRegExp("[^-]width\s*:\s*[^;]*", "i").test(';' + item.attrs.style))
|
||||
item.attrs.style += ';width:100%';
|
||||
return [item];
|
||||
}
|
||||
// Parser/trees/cssHandler.wxs
|
||||
module.exports = {
|
||||
getStyle: function(style, display) {
|
||||
var res = "";
|
||||
var reg = getRegExp("float\s*:\s*[^;]*", "i");
|
||||
if (reg.test(style)) res += reg.exec(style)[0];
|
||||
reg = getRegExp("margin[^;]*auto", "i");
|
||||
if (reg.test(style)) res += (";" + reg.exec(style)[0]);
|
||||
reg = getRegExp("display\s*:\s*([^;]*)", "i");
|
||||
if (reg.test(style) && reg.exec(style)[1]!="flex") res += (';' + reg.exec(style)[0]);
|
||||
else res += (';display:' + display);
|
||||
reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig");
|
||||
var width = reg.exec(style);
|
||||
while (width) {
|
||||
res += (';' + width[0]);
|
||||
width = reg.exec(style);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
setImgStyle: function(item, imgMode) {
|
||||
if (getRegExp("[^-]width\s*:\s*[^;]*", "i").test(';' + item.attrs.style))
|
||||
item.attrs.style += ';width:100%';
|
||||
if (imgMode == "widthFix")
|
||||
item.attrs.style += ";height:auto !important";
|
||||
return [item];
|
||||
},
|
||||
setStyle: function(item) {
|
||||
if (getRegExp("[^-]width\s*:\s*[^;]*", "i").test(';' + item.attrs.style))
|
||||
item.attrs.style += ';width:100%';
|
||||
return [item];
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,69 +1,69 @@
|
||||
/* 链接受到点击的hover-class,可自定义修改 */
|
||||
.navigator-hover {
|
||||
opacity: 0.7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* 以下内容不建议修改 */
|
||||
:host {
|
||||
display: inherit;
|
||||
float: inherit;
|
||||
}
|
||||
.sub, .sup, .bdo, .bdi, .ruby, .rt {
|
||||
display: inline-block !important;
|
||||
}
|
||||
.div, .blockquote, .p{
|
||||
display:block;
|
||||
}
|
||||
.b, .strong {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
||||
.em, .i {
|
||||
display: inline;
|
||||
font-style: italic;
|
||||
}
|
||||
.del {
|
||||
display: inline;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.ins {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.code {
|
||||
display: inline;
|
||||
font-family: monospace;
|
||||
}
|
||||
.big {
|
||||
font-size:1.2em;
|
||||
display:inline;
|
||||
}
|
||||
.small {
|
||||
font-size:0.8em;
|
||||
display:inline;
|
||||
}
|
||||
.q, .span, .label, .abbr {
|
||||
display: inline;
|
||||
}
|
||||
.q::before {
|
||||
content: '"';
|
||||
}
|
||||
.q::after {
|
||||
content: '"';
|
||||
}
|
||||
.video {
|
||||
background-color: rgb(0, 0, 0);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.video-triangle {
|
||||
border-width: 15px 0 15px 30px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent transparent white;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin: -15px 0 0 -15px;
|
||||
}
|
||||
/* 链接受到点击的hover-class,可自定义修改 */
|
||||
.navigator-hover {
|
||||
opacity: 0.7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* 以下内容不建议修改 */
|
||||
:host {
|
||||
display: inherit;
|
||||
float: inherit;
|
||||
}
|
||||
.sub, .sup, .bdo, .bdi, .ruby, .rt {
|
||||
display: inline-block !important;
|
||||
}
|
||||
.div, .blockquote, .p{
|
||||
display:block;
|
||||
}
|
||||
.b, .strong {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
||||
.em, .i {
|
||||
display: inline;
|
||||
font-style: italic;
|
||||
}
|
||||
.del {
|
||||
display: inline;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.ins {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.code {
|
||||
display: inline;
|
||||
font-family: monospace;
|
||||
}
|
||||
.big {
|
||||
font-size:1.2em;
|
||||
display:inline;
|
||||
}
|
||||
.small {
|
||||
font-size:0.8em;
|
||||
display:inline;
|
||||
}
|
||||
.q, .span, .label, .abbr {
|
||||
display: inline;
|
||||
}
|
||||
.q::before {
|
||||
content: '"';
|
||||
}
|
||||
.q::after {
|
||||
content: '"';
|
||||
}
|
||||
.video {
|
||||
background-color: rgb(0, 0, 0);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.video-triangle {
|
||||
border-width: 15px 0 15px 30px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent transparent white;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin: -15px 0 0 -15px;
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 1487 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user