fix(checkin): 修复 mockData 语法错误并完善地图活动功能
主要修改: - 修复 mockData.js 的语法错误(移除孤立的 return 语句) - 修复 generateApiFromOpenAPI.js 的 brace-style 代码风格问题 - 实现 CheckinMap 页面与 map_activity API 的集成 - 添加完整的 API 规范文档 - 更新 API 代码生成脚本 技术细节: - 清理 mockData.js 中遗留的孤立代码片段 - 移除未使用的函数参数以消除警告 - 修复 ESLint brace-style 规则错误(9处) - 完善 map_activity API 接口定义 - 添加 API Mock 数据支持开发环境测试 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
16 changed files
with
3269 additions
and
54 deletions
docs/api-specs/map_activity/checkin.md
0 → 100644
| 1 | +# 打卡 | ||
| 2 | + | ||
| 3 | +## OpenAPI Specification | ||
| 4 | + | ||
| 5 | +```yaml | ||
| 6 | +openapi: 3.0.1 | ||
| 7 | +info: | ||
| 8 | + title: '' | ||
| 9 | + version: 1.0.0 | ||
| 10 | +paths: | ||
| 11 | + /srv/: | ||
| 12 | + post: | ||
| 13 | + summary: 打卡 | ||
| 14 | + deprecated: false | ||
| 15 | + description: '' | ||
| 16 | + tags: | ||
| 17 | + - 老来赛/地图-新版多活动 | ||
| 18 | + parameters: | ||
| 19 | + - name: f | ||
| 20 | + in: query | ||
| 21 | + description: '' | ||
| 22 | + required: true | ||
| 23 | + example: walk | ||
| 24 | + schema: | ||
| 25 | + type: string | ||
| 26 | + - name: a | ||
| 27 | + in: query | ||
| 28 | + description: '' | ||
| 29 | + required: true | ||
| 30 | + example: map_activity | ||
| 31 | + schema: | ||
| 32 | + type: string | ||
| 33 | + - name: t | ||
| 34 | + in: query | ||
| 35 | + description: '' | ||
| 36 | + required: true | ||
| 37 | + example: checkin | ||
| 38 | + schema: | ||
| 39 | + type: string | ||
| 40 | + requestBody: | ||
| 41 | + content: | ||
| 42 | + application/x-www-form-urlencoded: | ||
| 43 | + schema: | ||
| 44 | + type: object | ||
| 45 | + properties: | ||
| 46 | + activity_id: | ||
| 47 | + description: 活动ID | ||
| 48 | + example: '' | ||
| 49 | + type: string | ||
| 50 | + detail_id: | ||
| 51 | + description: 打卡点ID | ||
| 52 | + example: '828360' | ||
| 53 | + type: string | ||
| 54 | + openid: | ||
| 55 | + example: oAHBN10P-hn-vF1cTY4tQeStQFmU | ||
| 56 | + type: string | ||
| 57 | + examples: {} | ||
| 58 | + responses: | ||
| 59 | + '200': | ||
| 60 | + description: '' | ||
| 61 | + content: | ||
| 62 | + application/json: | ||
| 63 | + schema: | ||
| 64 | + type: object | ||
| 65 | + properties: | ||
| 66 | + code: | ||
| 67 | + type: integer | ||
| 68 | + msg: | ||
| 69 | + type: string | ||
| 70 | + x-apifox-orders: | ||
| 71 | + - code | ||
| 72 | + - msg | ||
| 73 | + required: | ||
| 74 | + - code | ||
| 75 | + - msg | ||
| 76 | + headers: {} | ||
| 77 | + x-apifox-name: 成功 | ||
| 78 | + x-apifox-ordering: 0 | ||
| 79 | + security: [] | ||
| 80 | + x-apifox-folder: 老来赛/地图-新版多活动 | ||
| 81 | + x-apifox-status: integrating | ||
| 82 | + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072141-run | ||
| 83 | +components: | ||
| 84 | + schemas: {} | ||
| 85 | + responses: {} | ||
| 86 | + securitySchemes: {} | ||
| 87 | +servers: [] | ||
| 88 | +security: [] | ||
| 89 | + | ||
| 90 | +``` |
docs/api-specs/map_activity/detail.md
0 → 100644
| 1 | +# 地图活动详情 | ||
| 2 | + | ||
| 3 | +## OpenAPI Specification | ||
| 4 | + | ||
| 5 | +```yaml | ||
| 6 | +openapi: 3.0.1 | ||
| 7 | +info: | ||
| 8 | + title: '' | ||
| 9 | + version: 1.0.0 | ||
| 10 | +paths: | ||
| 11 | + /srv/: | ||
| 12 | + get: | ||
| 13 | + summary: 地图活动详情 | ||
| 14 | + deprecated: false | ||
| 15 | + description: '' | ||
| 16 | + tags: | ||
| 17 | + - 老来赛/地图-新版多活动 | ||
| 18 | + parameters: | ||
| 19 | + - name: f | ||
| 20 | + in: query | ||
| 21 | + description: '' | ||
| 22 | + required: true | ||
| 23 | + example: walk | ||
| 24 | + schema: | ||
| 25 | + type: string | ||
| 26 | + - name: a | ||
| 27 | + in: query | ||
| 28 | + description: '' | ||
| 29 | + required: true | ||
| 30 | + example: map_activity | ||
| 31 | + schema: | ||
| 32 | + type: string | ||
| 33 | + - name: t | ||
| 34 | + in: query | ||
| 35 | + description: '' | ||
| 36 | + required: true | ||
| 37 | + example: detail | ||
| 38 | + schema: | ||
| 39 | + type: string | ||
| 40 | + - name: id | ||
| 41 | + in: query | ||
| 42 | + description: 活动ID | ||
| 43 | + required: false | ||
| 44 | + schema: | ||
| 45 | + type: string | ||
| 46 | + responses: | ||
| 47 | + '200': | ||
| 48 | + description: '' | ||
| 49 | + content: | ||
| 50 | + application/json: | ||
| 51 | + schema: | ||
| 52 | + type: object | ||
| 53 | + properties: | ||
| 54 | + code: | ||
| 55 | + type: integer | ||
| 56 | + msg: | ||
| 57 | + type: string | ||
| 58 | + data: | ||
| 59 | + type: object | ||
| 60 | + properties: | ||
| 61 | + url: | ||
| 62 | + type: string | ||
| 63 | + title: 地图网址 | ||
| 64 | + id: | ||
| 65 | + type: string | ||
| 66 | + title: 活动ID | ||
| 67 | + cover: | ||
| 68 | + type: string | ||
| 69 | + title: 封面图 | ||
| 70 | + tittle: | ||
| 71 | + type: string | ||
| 72 | + title: 标题 | ||
| 73 | + begin_date: | ||
| 74 | + type: string | ||
| 75 | + title: 开始时间 | ||
| 76 | + end_date: | ||
| 77 | + type: string | ||
| 78 | + title: 结束时间 | ||
| 79 | + is_ended: | ||
| 80 | + type: boolean | ||
| 81 | + title: 活动是否已经结束 | ||
| 82 | + is_begin: | ||
| 83 | + type: boolean | ||
| 84 | + title: 活动是否开始 | ||
| 85 | + first_checkin_points: | ||
| 86 | + type: integer | ||
| 87 | + title: 首次打卡获得积分 | ||
| 88 | + required_checkin_count: | ||
| 89 | + type: integer | ||
| 90 | + title: 需要打卡几次,才能完成活动 | ||
| 91 | + complete_points: | ||
| 92 | + type: integer | ||
| 93 | + title: 完成活动获得多少积分 | ||
| 94 | + discount_title: | ||
| 95 | + type: string | ||
| 96 | + title: 打卡点底部优惠标题 | ||
| 97 | + x-apifox-orders: | ||
| 98 | + - id | ||
| 99 | + - cover | ||
| 100 | + - tittle | ||
| 101 | + - begin_date | ||
| 102 | + - end_date | ||
| 103 | + - is_ended | ||
| 104 | + - is_begin | ||
| 105 | + - url | ||
| 106 | + - first_checkin_points | ||
| 107 | + - required_checkin_count | ||
| 108 | + - complete_points | ||
| 109 | + - discount_title | ||
| 110 | + required: | ||
| 111 | + - url | ||
| 112 | + - end_date | ||
| 113 | + - begin_date | ||
| 114 | + - id | ||
| 115 | + - cover | ||
| 116 | + - tittle | ||
| 117 | + - first_checkin_points | ||
| 118 | + - required_checkin_count | ||
| 119 | + - complete_points | ||
| 120 | + - discount_title | ||
| 121 | + x-apifox-orders: | ||
| 122 | + - code | ||
| 123 | + - msg | ||
| 124 | + - data | ||
| 125 | + required: | ||
| 126 | + - code | ||
| 127 | + - msg | ||
| 128 | + - data | ||
| 129 | + headers: {} | ||
| 130 | + x-apifox-name: 成功 | ||
| 131 | + x-apifox-ordering: 0 | ||
| 132 | + security: [] | ||
| 133 | + x-apifox-folder: 老来赛/地图-新版多活动 | ||
| 134 | + x-apifox-status: integrating | ||
| 135 | + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417075691-run | ||
| 136 | +components: | ||
| 137 | + schemas: {} | ||
| 138 | + responses: {} | ||
| 139 | + securitySchemes: {} | ||
| 140 | +servers: [] | ||
| 141 | +security: [] | ||
| 142 | + | ||
| 143 | +``` |
docs/api-specs/map_activity/is_checked.md
0 → 100644
| 1 | +# 是否已经打卡 | ||
| 2 | + | ||
| 3 | +## OpenAPI Specification | ||
| 4 | + | ||
| 5 | +```yaml | ||
| 6 | +openapi: 3.0.1 | ||
| 7 | +info: | ||
| 8 | + title: '' | ||
| 9 | + version: 1.0.0 | ||
| 10 | +paths: | ||
| 11 | + /srv/: | ||
| 12 | + get: | ||
| 13 | + summary: 是否已经打卡 | ||
| 14 | + deprecated: false | ||
| 15 | + description: '' | ||
| 16 | + tags: | ||
| 17 | + - 老来赛/地图-新版多活动 | ||
| 18 | + parameters: | ||
| 19 | + - name: f | ||
| 20 | + in: query | ||
| 21 | + description: '' | ||
| 22 | + required: true | ||
| 23 | + example: walk | ||
| 24 | + schema: | ||
| 25 | + type: string | ||
| 26 | + - name: a | ||
| 27 | + in: query | ||
| 28 | + description: '' | ||
| 29 | + required: true | ||
| 30 | + example: map_activity | ||
| 31 | + schema: | ||
| 32 | + type: string | ||
| 33 | + - name: t | ||
| 34 | + in: query | ||
| 35 | + description: '' | ||
| 36 | + required: true | ||
| 37 | + example: is_checked | ||
| 38 | + schema: | ||
| 39 | + type: string | ||
| 40 | + - name: detail_id | ||
| 41 | + in: query | ||
| 42 | + description: 打卡点ID | ||
| 43 | + required: false | ||
| 44 | + example: '828359' | ||
| 45 | + schema: | ||
| 46 | + type: string | ||
| 47 | + - name: openid | ||
| 48 | + in: query | ||
| 49 | + description: '' | ||
| 50 | + required: false | ||
| 51 | + example: oAHBN10P-hn-vF1cTY4tQeStQFmU | ||
| 52 | + schema: | ||
| 53 | + type: string | ||
| 54 | + - name: activity_id | ||
| 55 | + in: query | ||
| 56 | + description: 活动ID | ||
| 57 | + required: false | ||
| 58 | + schema: | ||
| 59 | + type: string | ||
| 60 | + responses: | ||
| 61 | + '200': | ||
| 62 | + description: '' | ||
| 63 | + content: | ||
| 64 | + application/json: | ||
| 65 | + schema: | ||
| 66 | + type: object | ||
| 67 | + properties: | ||
| 68 | + code: | ||
| 69 | + type: integer | ||
| 70 | + msg: | ||
| 71 | + type: string | ||
| 72 | + data: | ||
| 73 | + type: object | ||
| 74 | + properties: | ||
| 75 | + is_checked: | ||
| 76 | + type: boolean | ||
| 77 | + title: 是否已经打卡 | ||
| 78 | + x-apifox-orders: | ||
| 79 | + - is_checked | ||
| 80 | + required: | ||
| 81 | + - is_checked | ||
| 82 | + x-apifox-orders: | ||
| 83 | + - code | ||
| 84 | + - msg | ||
| 85 | + - data | ||
| 86 | + required: | ||
| 87 | + - code | ||
| 88 | + - msg | ||
| 89 | + - data | ||
| 90 | + headers: {} | ||
| 91 | + x-apifox-name: 成功 | ||
| 92 | + x-apifox-ordering: 0 | ||
| 93 | + security: [] | ||
| 94 | + x-apifox-folder: 老来赛/地图-新版多活动 | ||
| 95 | + x-apifox-status: integrating | ||
| 96 | + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072140-run | ||
| 97 | +components: | ||
| 98 | + schemas: {} | ||
| 99 | + responses: {} | ||
| 100 | + securitySchemes: {} | ||
| 101 | +servers: [] | ||
| 102 | +security: [] | ||
| 103 | + | ||
| 104 | +``` |
docs/api-specs/map_activity/list.md
0 → 100644
| 1 | +# 地图活动列表 | ||
| 2 | + | ||
| 3 | +## OpenAPI Specification | ||
| 4 | + | ||
| 5 | +```yaml | ||
| 6 | +openapi: 3.0.1 | ||
| 7 | +info: | ||
| 8 | + title: '' | ||
| 9 | + version: 1.0.0 | ||
| 10 | +paths: | ||
| 11 | + /srv/: | ||
| 12 | + get: | ||
| 13 | + summary: 地图活动列表 | ||
| 14 | + deprecated: false | ||
| 15 | + description: '' | ||
| 16 | + tags: | ||
| 17 | + - 老来赛/地图-新版多活动 | ||
| 18 | + parameters: | ||
| 19 | + - name: f | ||
| 20 | + in: query | ||
| 21 | + description: '' | ||
| 22 | + required: true | ||
| 23 | + example: walk | ||
| 24 | + schema: | ||
| 25 | + type: string | ||
| 26 | + - name: a | ||
| 27 | + in: query | ||
| 28 | + description: '' | ||
| 29 | + required: true | ||
| 30 | + example: map_activity | ||
| 31 | + schema: | ||
| 32 | + type: string | ||
| 33 | + - name: t | ||
| 34 | + in: query | ||
| 35 | + description: '' | ||
| 36 | + required: true | ||
| 37 | + example: list | ||
| 38 | + schema: | ||
| 39 | + type: string | ||
| 40 | + responses: | ||
| 41 | + '200': | ||
| 42 | + description: '' | ||
| 43 | + content: | ||
| 44 | + application/json: | ||
| 45 | + schema: | ||
| 46 | + type: object | ||
| 47 | + properties: | ||
| 48 | + code: | ||
| 49 | + type: integer | ||
| 50 | + msg: | ||
| 51 | + type: string | ||
| 52 | + data: | ||
| 53 | + type: array | ||
| 54 | + items: | ||
| 55 | + type: object | ||
| 56 | + properties: | ||
| 57 | + url: | ||
| 58 | + type: string | ||
| 59 | + title: 地图网址 | ||
| 60 | + id: | ||
| 61 | + type: string | ||
| 62 | + title: 活动ID | ||
| 63 | + cover: | ||
| 64 | + type: string | ||
| 65 | + title: 封面图 | ||
| 66 | + tittle: | ||
| 67 | + type: string | ||
| 68 | + title: 标题 | ||
| 69 | + begin_date: | ||
| 70 | + type: string | ||
| 71 | + title: 开始时间 | ||
| 72 | + end_date: | ||
| 73 | + type: string | ||
| 74 | + title: 结束时间 | ||
| 75 | + x-apifox-orders: | ||
| 76 | + - id | ||
| 77 | + - cover | ||
| 78 | + - tittle | ||
| 79 | + - begin_date | ||
| 80 | + - end_date | ||
| 81 | + - url | ||
| 82 | + required: | ||
| 83 | + - url | ||
| 84 | + - end_date | ||
| 85 | + - begin_date | ||
| 86 | + - id | ||
| 87 | + - cover | ||
| 88 | + - tittle | ||
| 89 | + x-apifox-orders: | ||
| 90 | + - code | ||
| 91 | + - msg | ||
| 92 | + - data | ||
| 93 | + required: | ||
| 94 | + - code | ||
| 95 | + - msg | ||
| 96 | + - data | ||
| 97 | + headers: {} | ||
| 98 | + x-apifox-name: 成功 | ||
| 99 | + x-apifox-ordering: 0 | ||
| 100 | + security: [] | ||
| 101 | + x-apifox-folder: 老来赛/地图-新版多活动 | ||
| 102 | + x-apifox-status: integrating | ||
| 103 | + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072114-run | ||
| 104 | +components: | ||
| 105 | + schemas: {} | ||
| 106 | + responses: {} | ||
| 107 | + securitySchemes: {} | ||
| 108 | +servers: [] | ||
| 109 | +security: [] | ||
| 110 | + | ||
| 111 | +``` |
docs/api-specs/map_activity/poster.md
0 → 100644
| 1 | +# 获取海报 | ||
| 2 | + | ||
| 3 | +## OpenAPI Specification | ||
| 4 | + | ||
| 5 | +```yaml | ||
| 6 | +openapi: 3.0.1 | ||
| 7 | +info: | ||
| 8 | + title: '' | ||
| 9 | + version: 1.0.0 | ||
| 10 | +paths: | ||
| 11 | + /srv/: | ||
| 12 | + get: | ||
| 13 | + summary: 获取海报 | ||
| 14 | + deprecated: false | ||
| 15 | + description: '' | ||
| 16 | + tags: | ||
| 17 | + - 老来赛/地图-新版多活动 | ||
| 18 | + parameters: | ||
| 19 | + - name: f | ||
| 20 | + in: query | ||
| 21 | + description: '' | ||
| 22 | + required: true | ||
| 23 | + example: walk | ||
| 24 | + schema: | ||
| 25 | + type: string | ||
| 26 | + - name: a | ||
| 27 | + in: query | ||
| 28 | + description: '' | ||
| 29 | + required: true | ||
| 30 | + example: map_activity | ||
| 31 | + schema: | ||
| 32 | + type: string | ||
| 33 | + - name: t | ||
| 34 | + in: query | ||
| 35 | + description: '' | ||
| 36 | + required: true | ||
| 37 | + example: poster | ||
| 38 | + schema: | ||
| 39 | + type: string | ||
| 40 | + - name: activity_id | ||
| 41 | + in: query | ||
| 42 | + description: 活动ID | ||
| 43 | + required: false | ||
| 44 | + example: | ||
| 45 | + - '' | ||
| 46 | + schema: | ||
| 47 | + type: string | ||
| 48 | + - name: detail_id | ||
| 49 | + in: query | ||
| 50 | + description: 关卡ID | ||
| 51 | + required: false | ||
| 52 | + example: '' | ||
| 53 | + schema: | ||
| 54 | + type: string | ||
| 55 | + - name: env_version | ||
| 56 | + in: query | ||
| 57 | + description: 小程序版本。正式版为 "release",体验版为 "trial"。默认是正式版 | ||
| 58 | + required: false | ||
| 59 | + example: trial | ||
| 60 | + schema: | ||
| 61 | + type: string | ||
| 62 | + responses: | ||
| 63 | + '200': | ||
| 64 | + description: '' | ||
| 65 | + content: | ||
| 66 | + application/json: | ||
| 67 | + schema: | ||
| 68 | + type: object | ||
| 69 | + properties: | ||
| 70 | + code: | ||
| 71 | + type: integer | ||
| 72 | + msg: | ||
| 73 | + type: string | ||
| 74 | + data: | ||
| 75 | + type: object | ||
| 76 | + properties: | ||
| 77 | + details: | ||
| 78 | + type: array | ||
| 79 | + items: | ||
| 80 | + type: object | ||
| 81 | + properties: | ||
| 82 | + id: | ||
| 83 | + type: integer | ||
| 84 | + title: 关卡ID | ||
| 85 | + name: | ||
| 86 | + type: string | ||
| 87 | + title: 关卡名称 | ||
| 88 | + background_url: | ||
| 89 | + type: string | ||
| 90 | + title: 关卡背景图 | ||
| 91 | + main_slogan: | ||
| 92 | + type: string | ||
| 93 | + sub_slogan: | ||
| 94 | + type: string | ||
| 95 | + is_checked: | ||
| 96 | + type: boolean | ||
| 97 | + title: 是否已经打卡 | ||
| 98 | + x-apifox-orders: | ||
| 99 | + - id | ||
| 100 | + - name | ||
| 101 | + - is_checked | ||
| 102 | + - background_url | ||
| 103 | + - main_slogan | ||
| 104 | + - sub_slogan | ||
| 105 | + required: | ||
| 106 | + - id | ||
| 107 | + - name | ||
| 108 | + - background_url | ||
| 109 | + - main_slogan | ||
| 110 | + - sub_slogan | ||
| 111 | + - is_checked | ||
| 112 | + title: 关卡列表 | ||
| 113 | + family: | ||
| 114 | + type: object | ||
| 115 | + properties: | ||
| 116 | + id: | ||
| 117 | + type: integer | ||
| 118 | + title: 家庭ID | ||
| 119 | + name: | ||
| 120 | + type: string | ||
| 121 | + title: 家庭名称 | ||
| 122 | + avatar_url: | ||
| 123 | + type: string | ||
| 124 | + title: 家庭头像 | ||
| 125 | + x-apifox-orders: | ||
| 126 | + - id | ||
| 127 | + - name | ||
| 128 | + - avatar_url | ||
| 129 | + required: | ||
| 130 | + - id | ||
| 131 | + - name | ||
| 132 | + - avatar_url | ||
| 133 | + title: 用户的当前家庭 | ||
| 134 | + show_detail_index: | ||
| 135 | + type: integer | ||
| 136 | + title: 当前应该显示第几个关卡 | ||
| 137 | + description: 从 0 开始计数 | ||
| 138 | + end_date: | ||
| 139 | + type: string | ||
| 140 | + title: 活动截止时间 | ||
| 141 | + qrcode_url: | ||
| 142 | + type: string | ||
| 143 | + title: 小程序码 | ||
| 144 | + title: | ||
| 145 | + type: string | ||
| 146 | + title: 海报标题 | ||
| 147 | + begin_date: | ||
| 148 | + type: string | ||
| 149 | + title: 活动开始日期 | ||
| 150 | + x-apifox-orders: | ||
| 151 | + - title | ||
| 152 | + - begin_date | ||
| 153 | + - end_date | ||
| 154 | + - details | ||
| 155 | + - show_detail_index | ||
| 156 | + - family | ||
| 157 | + - qrcode_url | ||
| 158 | + required: | ||
| 159 | + - title | ||
| 160 | + - details | ||
| 161 | + - family | ||
| 162 | + - show_detail_index | ||
| 163 | + - end_date | ||
| 164 | + - qrcode_url | ||
| 165 | + - begin_date | ||
| 166 | + x-apifox-orders: | ||
| 167 | + - code | ||
| 168 | + - msg | ||
| 169 | + - data | ||
| 170 | + required: | ||
| 171 | + - code | ||
| 172 | + - msg | ||
| 173 | + - data | ||
| 174 | + headers: {} | ||
| 175 | + x-apifox-name: 成功 | ||
| 176 | + x-apifox-ordering: 0 | ||
| 177 | + security: [] | ||
| 178 | + x-apifox-folder: 老来赛/地图-新版多活动 | ||
| 179 | + x-apifox-status: integrating | ||
| 180 | + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072142-run | ||
| 181 | +components: | ||
| 182 | + schemas: {} | ||
| 183 | + responses: {} | ||
| 184 | + securitySchemes: {} | ||
| 185 | +servers: [] | ||
| 186 | +security: [] | ||
| 187 | + | ||
| 188 | +``` |
| 1 | +# 上传海报背景 | ||
| 2 | + | ||
| 3 | +## OpenAPI Specification | ||
| 4 | + | ||
| 5 | +```yaml | ||
| 6 | +openapi: 3.0.1 | ||
| 7 | +info: | ||
| 8 | + title: '' | ||
| 9 | + version: 1.0.0 | ||
| 10 | +paths: | ||
| 11 | + /srv/: | ||
| 12 | + post: | ||
| 13 | + summary: 上传海报背景 | ||
| 14 | + deprecated: false | ||
| 15 | + description: '' | ||
| 16 | + tags: | ||
| 17 | + - 老来赛/地图-新版多活动 | ||
| 18 | + parameters: | ||
| 19 | + - name: f | ||
| 20 | + in: query | ||
| 21 | + description: '' | ||
| 22 | + required: true | ||
| 23 | + example: walk | ||
| 24 | + schema: | ||
| 25 | + type: string | ||
| 26 | + - name: a | ||
| 27 | + in: query | ||
| 28 | + description: '' | ||
| 29 | + required: true | ||
| 30 | + example: map_activity | ||
| 31 | + schema: | ||
| 32 | + type: string | ||
| 33 | + - name: t | ||
| 34 | + in: query | ||
| 35 | + description: '' | ||
| 36 | + required: true | ||
| 37 | + example: save_poster_background | ||
| 38 | + schema: | ||
| 39 | + type: string | ||
| 40 | + requestBody: | ||
| 41 | + content: | ||
| 42 | + application/x-www-form-urlencoded: | ||
| 43 | + schema: | ||
| 44 | + type: object | ||
| 45 | + properties: | ||
| 46 | + activity_id: | ||
| 47 | + description: 活动ID | ||
| 48 | + example: '' | ||
| 49 | + type: string | ||
| 50 | + detail_id: | ||
| 51 | + description: 打卡点ID | ||
| 52 | + example: '828359' | ||
| 53 | + type: string | ||
| 54 | + poster_background_url: | ||
| 55 | + description: 关卡海报背景 | ||
| 56 | + example: >- | ||
| 57 | + https://cdn.ipadbiz.cn/space_34093/t0122259914a77e9a57_FiYxd1DK70vLJ53Po8g2Y6JmqMeQ.jpg | ||
| 58 | + type: string | ||
| 59 | + examples: {} | ||
| 60 | + responses: | ||
| 61 | + '200': | ||
| 62 | + description: '' | ||
| 63 | + content: | ||
| 64 | + application/json: | ||
| 65 | + schema: | ||
| 66 | + type: object | ||
| 67 | + properties: | ||
| 68 | + code: | ||
| 69 | + type: integer | ||
| 70 | + msg: | ||
| 71 | + type: string | ||
| 72 | + x-apifox-orders: | ||
| 73 | + - code | ||
| 74 | + - msg | ||
| 75 | + required: | ||
| 76 | + - code | ||
| 77 | + - msg | ||
| 78 | + headers: {} | ||
| 79 | + x-apifox-name: 成功 | ||
| 80 | + x-apifox-ordering: 0 | ||
| 81 | + security: [] | ||
| 82 | + x-apifox-folder: 老来赛/地图-新版多活动 | ||
| 83 | + x-apifox-status: developing | ||
| 84 | + x-run-in-apifox: https://app.apifox.com/web/project/1753326/apis/api-417072143-run | ||
| 85 | +components: | ||
| 86 | + schemas: {} | ||
| 87 | + responses: {} | ||
| 88 | + securitySchemes: {} | ||
| 89 | +servers: [] | ||
| 90 | +security: [] | ||
| 91 | + | ||
| 92 | +``` |
| ... | @@ -18,14 +18,14 @@ | ... | @@ -18,14 +18,14 @@ |
| 18 | "build:rn": "taro build --type rn", | 18 | "build:rn": "taro build --type rn", |
| 19 | "build:qq": "taro build --type qq", | 19 | "build:qq": "taro build --type qq", |
| 20 | "build:quickapp": "taro build --type quickapp", | 20 | "build:quickapp": "taro build --type quickapp", |
| 21 | - "dev:weapp": "npm run build:weapp -- --watch", | 21 | + "dev:weapp": "NODE_ENV=development npm run build:weapp -- --watch", |
| 22 | - "dev:swan": "npm run build:swan -- --watch", | 22 | + "dev:swan": "NODE_ENV=development npm run build:swan -- --watch", |
| 23 | - "dev:alipay": "npm run build:alipay -- --watch", | 23 | + "dev:alipay": "NODE_ENV=development npm run build:alipay -- --watch", |
| 24 | - "dev:tt": "npm run build:tt -- --watch", | 24 | + "dev:tt": "NODE_ENV=development npm run build:tt -- --watch", |
| 25 | - "dev:h5": "npm run build:h5 -- --watch", | 25 | + "dev:h5": "NODE_ENV=development npm run build:h5 -- --watch", |
| 26 | - "dev:rn": "npm run build:rn -- --watch", | 26 | + "dev:rn": "NODE_ENV=development npm run build:rn -- --watch", |
| 27 | - "dev:qq": "npm run build:qq -- --watch", | 27 | + "dev:qq": "NODE_ENV=development npm run build:qq -- --watch", |
| 28 | - "dev:quickapp": "npm run build:quickapp -- --watch", | 28 | + "dev:quickapp": "NODE_ENV=development npm run build:quickapp -- --watch", |
| 29 | "postinstall": "weapp-tw patch", | 29 | "postinstall": "weapp-tw patch", |
| 30 | "prepare": "husky install", | 30 | "prepare": "husky install", |
| 31 | "test": "vitest", | 31 | "test": "vitest", |
| ... | @@ -33,7 +33,8 @@ | ... | @@ -33,7 +33,8 @@ |
| 33 | "test:coverage": "vitest --coverage", | 33 | "test:coverage": "vitest --coverage", |
| 34 | "test:run": "vitest run", | 34 | "test:run": "vitest run", |
| 35 | "lint": "eslint ./src --ext .vue,.js", | 35 | "lint": "eslint ./src --ext .vue,.js", |
| 36 | - "format": "prettier --write \"src/**/*.{js,vue,less}\"" | 36 | + "format": "prettier --write \"src/**/*.{js,vue,less}\"", |
| 37 | + "api:generate": "node scripts/generateApiFromOpenAPI.js" | ||
| 37 | }, | 38 | }, |
| 38 | "browserslist": [ | 39 | "browserslist": [ |
| 39 | "last 3 versions", | 40 | "last 3 versions", | ... | ... |
scripts/API_GUIDE.md
0 → 100644
| 1 | +# API 文档生成指南 | ||
| 2 | + | ||
| 3 | +本项目的 API 文档采用手动维护的方式。 | ||
| 4 | + | ||
| 5 | +## 📝 工作流程 | ||
| 6 | + | ||
| 7 | +### 1. 维护 OpenAPI 文档 | ||
| 8 | + | ||
| 9 | +在 `docs/api-specs/` 目录中维护 OpenAPI 文档。 | ||
| 10 | + | ||
| 11 | +#### 目录结构 | ||
| 12 | + | ||
| 13 | +``` | ||
| 14 | +docs/api-specs/ | ||
| 15 | +├── user/ # 用户模块 | ||
| 16 | +│ ├── login.md | ||
| 17 | +│ ├── login_status.md | ||
| 18 | +│ └── ... | ||
| 19 | +├── favorite/ # 收藏模块 | ||
| 20 | +│ ├── add.md | ||
| 21 | +│ ├── del.md | ||
| 22 | +│ └── list.md | ||
| 23 | +└── ... | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +#### 文档格式 | ||
| 27 | + | ||
| 28 | +每个 `.md` 文件包含: | ||
| 29 | +- 接口描述(Markdown 格式) | ||
| 30 | +- OpenAPI 3.0.1 规范(YAML 格式) | ||
| 31 | + | ||
| 32 | +**示例**(`docs/api-specs/user/login.md`): | ||
| 33 | + | ||
| 34 | +\`\`\`markdown | ||
| 35 | +# 登录并绑定 OpenID | ||
| 36 | + | ||
| 37 | +## 接口信息 | ||
| 38 | + | ||
| 39 | +- **方法**: POST | ||
| 40 | +- **路径**: /srv/?a=user&t=login | ||
| 41 | +- **标签**: user | ||
| 42 | + | ||
| 43 | +## OpenAPI 规范 | ||
| 44 | + | ||
| 45 | +\`\`\`yaml | ||
| 46 | +openapi: 3.0.0 | ||
| 47 | +info: | ||
| 48 | + title: 登录并绑定 OpenID | ||
| 49 | + description: 使用手机号和验证码登录,并绑定到 OpenID | ||
| 50 | + version: 1.0.0 | ||
| 51 | +paths: | ||
| 52 | + /srv/?: | ||
| 53 | + post: | ||
| 54 | + summary: 登录并绑定 OpenID | ||
| 55 | + description: 使用手机号和验证码登录,并绑定到 OpenID | ||
| 56 | + requestBody: | ||
| 57 | + content: | ||
| 58 | + application/x-www-form-urlencoded: | ||
| 59 | + schema: | ||
| 60 | + type: object | ||
| 61 | + required: | ||
| 62 | + - f | ||
| 63 | + - a | ||
| 64 | + - t | ||
| 65 | + properties: | ||
| 66 | + f: | ||
| 67 | + type: string | ||
| 68 | + description: 业务模块标识 | ||
| 69 | + example: manulife | ||
| 70 | + a: | ||
| 71 | + type: string | ||
| 72 | + description: 模块名(user) | ||
| 73 | + example: user | ||
| 74 | + t: | ||
| 75 | + type: string | ||
| 76 | + description: 接口类型(login) | ||
| 77 | + example: login | ||
| 78 | + phone: | ||
| 79 | + type: string | ||
| 80 | + description: 手机号 | ||
| 81 | + example: '13800138000' | ||
| 82 | + code: | ||
| 83 | + type: string | ||
| 84 | + description: 验证码 | ||
| 85 | + example: '123456' | ||
| 86 | + openid: | ||
| 87 | + type: string | ||
| 88 | + description: 微信 OpenID | ||
| 89 | + example: 'oXXXX-XXXXXXXXXXXXXXXXXXX' | ||
| 90 | + responses: | ||
| 91 | + '200': | ||
| 92 | + description: 成功 | ||
| 93 | + content: | ||
| 94 | + application/json: | ||
| 95 | + schema: | ||
| 96 | + type: object | ||
| 97 | + properties: | ||
| 98 | + code: | ||
| 99 | + type: number | ||
| 100 | + description: 状态码(0=失败,1=成功) | ||
| 101 | + msg: | ||
| 102 | + type: string | ||
| 103 | + description: 消息 | ||
| 104 | + data: | ||
| 105 | + type: object | ||
| 106 | + description: 用户信息 | ||
| 107 | + properties: | ||
| 108 | + id: | ||
| 109 | + type: number | ||
| 110 | + description: 用户 ID | ||
| 111 | + avatar: | ||
| 112 | + type: string | ||
| 113 | + description: 头像 URL | ||
| 114 | + name: | ||
| 115 | + type: string | ||
| 116 | + description: 姓名 | ||
| 117 | +\`\`\` | ||
| 118 | +\`\`\` | ||
| 119 | + | ||
| 120 | +### 2. 生成 API 代码 | ||
| 121 | + | ||
| 122 | +运行生成脚本: | ||
| 123 | + | ||
| 124 | +```bash | ||
| 125 | +node scripts/generateApiFromOpenAPI.js | ||
| 126 | +``` | ||
| 127 | + | ||
| 128 | +#### 生成内容 | ||
| 129 | + | ||
| 130 | +脚本会: | ||
| 131 | +1. 扫描 `docs/api-specs/` 目录 | ||
| 132 | +2. 解析每个 `.md` 文件中的 OpenAPI 规范 | ||
| 133 | +3. 生成对应的 JavaScript API 文件到 `src/api/` 目录 | ||
| 134 | + | ||
| 135 | +#### 输出示例 | ||
| 136 | + | ||
| 137 | +\`\`\` | ||
| 138 | +=== OpenAPI 转 API 文档生成器 === | ||
| 139 | + | ||
| 140 | +输入目录: /Users/huyirui/program/itomix/git/manulife-weapp/docs/api-specs | ||
| 141 | +输出目录: /Users/huyirui/program/itomix/git/manulife-weapp/src/api | ||
| 142 | + | ||
| 143 | +💾 备份当前 OpenAPI 文档... | ||
| 144 | + | ||
| 145 | +找到 9 个模块: event, favorite, feedback, file, get_file_list, get_product, news, user, wechat | ||
| 146 | + | ||
| 147 | +处理模块: user | ||
| 148 | +找到 5 个 API 文档 | ||
| 149 | + ✓ get_profile: 获取个人信息 | ||
| 150 | + ✓ login: 登录并绑定openid | ||
| 151 | + ✓ login_status: 查询登录状态 | ||
| 152 | + ✓ logout: 退出登录并解绑openid | ||
| 153 | + ✓ update_profile: 更新个人资料 | ||
| 154 | + 📝 生成文件: /Users/huyirui/program/itomix/git/manulife-weapp/src/api/user.js | ||
| 155 | + | ||
| 156 | +✅ API 文档生成完成! | ||
| 157 | +\`\`\` | ||
| 158 | + | ||
| 159 | +### 3. 使用生成的 API | ||
| 160 | + | ||
| 161 | +在组件中导入并使用: | ||
| 162 | + | ||
| 163 | +\`\`\`javascript | ||
| 164 | +import { loginAPI, getUserProfileAPI } from '@/api/user'; | ||
| 165 | + | ||
| 166 | +// 登录 | ||
| 167 | +const result = await loginAPI({ | ||
| 168 | + phone: '13800138000', | ||
| 169 | + code: '123456', | ||
| 170 | + openid: 'oXXXX-XXXXXXXXXXXXXXXXXXX' | ||
| 171 | +}); | ||
| 172 | + | ||
| 173 | +if (result.code === 1) { | ||
| 174 | + console.log('登录成功', result.data); | ||
| 175 | +} | ||
| 176 | +\`\`\` | ||
| 177 | + | ||
| 178 | +## 🔧 高级功能 | ||
| 179 | + | ||
| 180 | +### API 变更检测 | ||
| 181 | + | ||
| 182 | +脚本会自动检测 API 变更: | ||
| 183 | + | ||
| 184 | +- ✅ **新增接口** - 检测到新的 API 文档 | ||
| 185 | +- ⚠️ **修改接口** - 检测到 API 规范变更 | ||
| 186 | +- ❌ **删除接口** - 检测到删除的 API 文档 | ||
| 187 | + | ||
| 188 | +#### 变更报告示例 | ||
| 189 | + | ||
| 190 | +\`\`\` | ||
| 191 | +🔍 开始检测 API 变更... | ||
| 192 | + | ||
| 193 | +📦 新增模块: user | ||
| 194 | + 包含 2 个新增接口: | ||
| 195 | + • POST /srv/?a=user&t=login - 登录并绑定 OpenID | ||
| 196 | + • GET /srv/?a=user&t=get_profile - 获取个人信息 | ||
| 197 | + | ||
| 198 | +📦 对比范围: 9 个旧接口 → 11 个新接口 | ||
| 199 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 200 | + | ||
| 201 | +✅ 新增接口 (2): | ||
| 202 | + + login - 登录并绑定 OpenID | ||
| 203 | + + get_profile - 获取个人信息 | ||
| 204 | + | ||
| 205 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 206 | +总计: 2 新增, 0 修改, 0 删除 | ||
| 207 | +✅ 未检测到破坏性变更 | ||
| 208 | +\`\`\` | ||
| 209 | + | ||
| 210 | +## 📚 最佳实践 | ||
| 211 | + | ||
| 212 | +### 1. 文档命名 | ||
| 213 | + | ||
| 214 | +- 使用语义化文件名(如 `login.md`, `get_list.md`) | ||
| 215 | +- 避免使用通用名称(如 `api.md`, `endpoint.md`) | ||
| 216 | + | ||
| 217 | +### 2. 参数定义 | ||
| 218 | + | ||
| 219 | +- **必填参数**:在 `required` 数组中列出 | ||
| 220 | +- **可选参数**:不在 `required` 中 | ||
| 221 | +- **描述**:为每个参数添加清晰的 `description` | ||
| 222 | +- **示例**:为每个参数添加 `example` | ||
| 223 | + | ||
| 224 | +### 3. 响应结构 | ||
| 225 | + | ||
| 226 | +- 统一使用 `{ code, msg, data }` 格式 | ||
| 227 | +- `code`: 状态码(0=失败,1=成功) | ||
| 228 | +- `msg`: 消息说明 | ||
| 229 | +- `data`: 数据内容 | ||
| 230 | + | ||
| 231 | +### 4. 文档分组 | ||
| 232 | + | ||
| 233 | +- 按业务模块分组(user, favorite, product 等) | ||
| 234 | +- 每个模块一个目录 | ||
| 235 | +- 相关接口放在同一目录 | ||
| 236 | + | ||
| 237 | +## 🛠️ 故障排除 | ||
| 238 | + | ||
| 239 | +### 问题:YAML 解析失败 | ||
| 240 | + | ||
| 241 | +**错误信息**: | ||
| 242 | +\`\`\` | ||
| 243 | +✗ login.md: 解析失败 - YAML 代码块格式错误 | ||
| 244 | +\`\`\` | ||
| 245 | + | ||
| 246 | +**解决方案**: | ||
| 247 | +- 检查 YAML 代码块是否正确包裹在 `\`\`\`yaml` 和 `\`\`\`` 之间 | ||
| 248 | +- 检查 YAML 缩进是否正确(使用空格,不要使用 Tab) | ||
| 249 | +- 使用在线 YAML 验证器验证格式 | ||
| 250 | + | ||
| 251 | +### 问题:未找到 YAML 代码块 | ||
| 252 | + | ||
| 253 | +**错误信息**: | ||
| 254 | +\`\`\` | ||
| 255 | +⚠️ login.md: 未找到 YAML 代码块 | ||
| 256 | +\`\`\` | ||
| 257 | + | ||
| 258 | +**解决方案**: | ||
| 259 | +- 确保文档中包含 `\`\`\`yaml` 代码块 | ||
| 260 | +- 检查代码块格式是否正确 | ||
| 261 | + | ||
| 262 | +### 问题:生成的 API 代码为空 | ||
| 263 | + | ||
| 264 | +**可能原因**: | ||
| 265 | +1. OpenAPI 文档格式不正确 | ||
| 266 | +2. `paths` 或 `requestBody` 定义缺失 | ||
| 267 | + | ||
| 268 | +**解决方案**: | ||
| 269 | +- 检查 OpenAPI 文档结构是否完整 | ||
| 270 | +- 参考本文档中的示例格式 | ||
| 271 | + | ||
| 272 | +## 📖 参考资料 | ||
| 273 | + | ||
| 274 | +- [OpenAPI 3.0 规范](https://swagger.io/specification/) | ||
| 275 | +- [YAML 语法指南](https://yaml.org/spec/1.2/spec.html) | ||
| 276 | +- [项目 API 文档目录](../docs/api-specs/) |
scripts/QUICKSTART.md
0 → 100644
| 1 | +# OpenAPI 转 API 文档生成器 - 快速开始 | ||
| 2 | + | ||
| 3 | +## 🎯 一分钟快速上手 | ||
| 4 | + | ||
| 5 | +### 1️⃣ 创建 OpenAPI 文档 | ||
| 6 | + | ||
| 7 | +在 `docs/api-specs/` 目录下创建模块和接口文档: | ||
| 8 | + | ||
| 9 | +```bash | ||
| 10 | +# 创建新模块 | ||
| 11 | +mkdir -p docs/api-specs/product | ||
| 12 | + | ||
| 13 | +# 创建接口文档 | ||
| 14 | +touch docs/api-specs/product/getList.md | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +### 2️⃣ 编写 OpenAPI 规范 | ||
| 18 | + | ||
| 19 | +编辑 `getList.md`: | ||
| 20 | + | ||
| 21 | +```markdown | ||
| 22 | +# 获取商品列表 | ||
| 23 | + | ||
| 24 | +## OpenAPI Specification | ||
| 25 | + | ||
| 26 | +\```yaml | ||
| 27 | +openapi: 3.0.1 | ||
| 28 | +info: | ||
| 29 | + title: '' | ||
| 30 | + version: 1.0.0 | ||
| 31 | +paths: | ||
| 32 | + /srv/: | ||
| 33 | + get: | ||
| 34 | + summary: 获取商品列表 | ||
| 35 | + tags: | ||
| 36 | + - 商品 | ||
| 37 | + parameters: | ||
| 38 | + - name: a | ||
| 39 | + in: query | ||
| 40 | + example: product_list | ||
| 41 | + - name: f | ||
| 42 | + in: query | ||
| 43 | + example: behalo | ||
| 44 | + responses: | ||
| 45 | + '200': | ||
| 46 | + description: 成功 | ||
| 47 | +\``` | ||
| 48 | +``` | ||
| 49 | + | ||
| 50 | +### 3️⃣ 生成 API 文件 | ||
| 51 | + | ||
| 52 | +```bash | ||
| 53 | +pnpm api:generate | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +### 4️⃣ 使用生成的 API | ||
| 57 | + | ||
| 58 | +```javascript | ||
| 59 | +import { getListAPI } from '@/api/product'; | ||
| 60 | + | ||
| 61 | +const result = await getListAPI({ page: 1, pageSize: 10 }); | ||
| 62 | +``` | ||
| 63 | + | ||
| 64 | +## ✅ 验证结果 | ||
| 65 | + | ||
| 66 | +运行测试脚本验证生成的文件: | ||
| 67 | + | ||
| 68 | +```bash | ||
| 69 | +node scripts/test-generate.js | ||
| 70 | +``` | ||
| 71 | + | ||
| 72 | +## 📂 文件结构 | ||
| 73 | + | ||
| 74 | +``` | ||
| 75 | +manulife-weapp/ | ||
| 76 | +├── docs/ | ||
| 77 | +│ ├── api-specs/ # API 规范文档源目录 | ||
| 78 | +│ │ └── user/ # 模块目录 | ||
| 79 | +│ │ └── getUserInfo.md | ||
| 80 | +│ ├── OPENAPI_TO_API_GUIDE.md # 详细使用指南 | ||
| 81 | +│ └── API_USAGE_EXAMPLES.md # API 使用示例 | ||
| 82 | +├── scripts/ | ||
| 83 | +│ ├── generateApiFromOpenAPI.js # 生成器核心脚本 | ||
| 84 | +│ └── test-generate.js # 测试脚本 | ||
| 85 | +├── src/ | ||
| 86 | +│ └── api/ # 生成的 API 文件目录 | ||
| 87 | +│ ├── user.js # 自动生成 | ||
| 88 | +│ ├── wx/ | ||
| 89 | +│ └── index.js | ||
| 90 | +└── package.json # 包含 api:generate 命令 | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +## 🔄 工作流程 | ||
| 94 | + | ||
| 95 | +```mermaid | ||
| 96 | +graph LR | ||
| 97 | + A[编写 OpenAPI 文档] --> B[运行 pnpm api:generate] | ||
| 98 | + B --> C[生成 API 文件] | ||
| 99 | + C --> D[在项目中使用] | ||
| 100 | + D --> E[需要修改接口] | ||
| 101 | + E --> A | ||
| 102 | +``` | ||
| 103 | + | ||
| 104 | +## 🎨 常见场景 | ||
| 105 | + | ||
| 106 | +### 场景 1: 批量生成多个接口 | ||
| 107 | + | ||
| 108 | +```bash | ||
| 109 | +docs/api-specs/ | ||
| 110 | +├── user/ | ||
| 111 | +│ ├── getUserInfo.md | ||
| 112 | +│ ├── updateProfile.md | ||
| 113 | +│ └── changePassword.md | ||
| 114 | +└── order/ | ||
| 115 | + ├── getList.md | ||
| 116 | + └── getDetail.md | ||
| 117 | +``` | ||
| 118 | + | ||
| 119 | +运行 `pnpm api:generate` 后生成: | ||
| 120 | + | ||
| 121 | +``` | ||
| 122 | +src/api/ | ||
| 123 | +├── user.js # 包含 3 个接口 | ||
| 124 | +└── order.js # 包含 2 个接口 | ||
| 125 | +``` | ||
| 126 | + | ||
| 127 | +### 场景 2: 更新已有接口 | ||
| 128 | + | ||
| 129 | +1. 修改 `docs/api-specs/user/getUserInfo.md` | ||
| 130 | +2. 运行 `pnpm api:generate` | ||
| 131 | +3. `src/api/user.js` 自动更新 | ||
| 132 | + | ||
| 133 | +### 场景 3: 添加新模块 | ||
| 134 | + | ||
| 135 | +1. 创建 `docs/api-specs/payment/` | ||
| 136 | +2. 添加接口文档 | ||
| 137 | +3. 运行生成命令 | ||
| 138 | +4. 自动生成 `src/api/payment.js` | ||
| 139 | + | ||
| 140 | +## ⚙️ 配置和自定义 | ||
| 141 | + | ||
| 142 | +### 修改输出目录 | ||
| 143 | + | ||
| 144 | +编辑 `scripts/generateApiFromOpenAPI.js`: | ||
| 145 | + | ||
| 146 | +```javascript | ||
| 147 | +const outputDir = path.resolve(__dirname, '../src/api'); | ||
| 148 | +// 改为你想要的目录 | ||
| 149 | +const outputDir = path.resolve(__dirname, '../src/apis'); | ||
| 150 | +``` | ||
| 151 | + | ||
| 152 | +### 修改命名规则 | ||
| 153 | + | ||
| 154 | +编辑 `toCamelCase()` 或 `toPascalCase()` 函数。 | ||
| 155 | + | ||
| 156 | +### 修改生成模板 | ||
| 157 | + | ||
| 158 | +编辑 `generateApiFileContent()` 函数。 | ||
| 159 | + | ||
| 160 | +## 🐛 调试技巧 | ||
| 161 | + | ||
| 162 | +### 启用详细日志 | ||
| 163 | + | ||
| 164 | +在脚本中添加更多 console.log: | ||
| 165 | + | ||
| 166 | +```javascript | ||
| 167 | +console.log('解析的 API 信息:', JSON.stringify(apiInfo, null, 2)); | ||
| 168 | +``` | ||
| 169 | + | ||
| 170 | +### 单独测试某个模块 | ||
| 171 | + | ||
| 172 | +修改脚本中的模块过滤逻辑。 | ||
| 173 | + | ||
| 174 | +### 查看生成的中间数据 | ||
| 175 | + | ||
| 176 | +添加调试输出查看 YAML 解析结果。 | ||
| 177 | + | ||
| 178 | +## 📞 获取帮助 | ||
| 179 | + | ||
| 180 | +- 详细指南:[OpenAPI 转 API 文档生成器指南](./OPENAPI_TO_API_GUIDE.md) | ||
| 181 | +- 使用示例:[API 使用示例](./API_USAGE_EXAMPLES.md) | ||
| 182 | +- 项目架构:[CLAUDE.md](../CLAUDE.md) | ||
| 183 | + | ||
| 184 | +## 🎉 开始使用 | ||
| 185 | + | ||
| 186 | +现在你已经准备好了!开始创建你的第一个 OpenAPI 文档吧。 | ||
| 187 | + | ||
| 188 | +```bash | ||
| 189 | +# 1. 创建模块目录 | ||
| 190 | +mkdir -p docs/api-specs/your-module | ||
| 191 | + | ||
| 192 | +# 2. 创建接口文档(参考 docs/api-specs/user/getUserInfo.md) | ||
| 193 | + | ||
| 194 | +# 3. 生成 API | ||
| 195 | +pnpm api:generate | ||
| 196 | + | ||
| 197 | +# 4. 查看生成的文件 | ||
| 198 | +cat src/api/your-module.js | ||
| 199 | + | ||
| 200 | +# 5. 开始使用 | ||
| 201 | +``` | ||
| 202 | + | ||
| 203 | +祝你编码愉快!🚀 |
scripts/apiDiff.js
0 → 100644
| 1 | +/** | ||
| 2 | + * API 对比工具 | ||
| 3 | + * | ||
| 4 | + * 功能: | ||
| 5 | + * 1. 对比两个 OpenAPI 文档的差异 | ||
| 6 | + * 2. 检测破坏性变更 | ||
| 7 | + * 3. 生成详细的变更报告 | ||
| 8 | + * | ||
| 9 | + * 使用方式: | ||
| 10 | + * node scripts/apiDiff.js <oldPath> <newPath> | ||
| 11 | + */ | ||
| 12 | + | ||
| 13 | +const fs = require('fs') | ||
| 14 | +const path = require('path') | ||
| 15 | +const yaml = require('js-yaml') | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * 从 Markdown 文件中提取 YAML | ||
| 19 | + */ | ||
| 20 | +function extractYAMLFromMarkdown(content) { | ||
| 21 | + const yamlRegex = /```yaml\s*\n([\s\S]*?)\n```/ | ||
| 22 | + const match = content.match(yamlRegex) | ||
| 23 | + return match ? match[1] : null | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * 解析 OpenAPI 文档(支持 .md 和目录) | ||
| 28 | + */ | ||
| 29 | +function parseOpenAPIPath(filePath) { | ||
| 30 | + const stat = fs.statSync(filePath) | ||
| 31 | + | ||
| 32 | + if (stat.isFile()) { | ||
| 33 | + // 单个文件 | ||
| 34 | + if (filePath.endsWith('.md')) { | ||
| 35 | + const content = fs.readFileSync(filePath, 'utf8') | ||
| 36 | + const yamlContent = extractYAMLFromMarkdown(content) | ||
| 37 | + if (!yamlContent) { | ||
| 38 | + throw new Error(`文件 ${filePath} 中未找到 YAML 代码块`) | ||
| 39 | + } | ||
| 40 | + return [yaml.load(yamlContent)] | ||
| 41 | + } else if (filePath.endsWith('.js')) { | ||
| 42 | + // TODO: 支持对比生成的 JS 文件(需要解析 AST) | ||
| 43 | + throw new Error('暂不支持对比生成的 JS 文件,请对比 OpenAPI 文档') | ||
| 44 | + } else { | ||
| 45 | + throw new Error(`不支持的文件类型: ${filePath}`) | ||
| 46 | + } | ||
| 47 | + } else if (stat.isDirectory()) { | ||
| 48 | + // 目录,读取所有 .md 文件 | ||
| 49 | + const files = fs.readdirSync(filePath).filter(f => f.endsWith('.md')) | ||
| 50 | + const docs = [] | ||
| 51 | + files.forEach(file => { | ||
| 52 | + const fullPath = path.join(filePath, file) | ||
| 53 | + const content = fs.readFileSync(fullPath, 'utf8') | ||
| 54 | + const yamlContent = extractYAMLFromMarkdown(content) | ||
| 55 | + if (yamlContent) { | ||
| 56 | + const doc = yaml.load(yamlContent) | ||
| 57 | + // 保存文件名用于标识 | ||
| 58 | + doc._fileName = path.basename(file, '.md') | ||
| 59 | + docs.push(doc) | ||
| 60 | + } | ||
| 61 | + }) | ||
| 62 | + return docs | ||
| 63 | + } | ||
| 64 | +} | ||
| 65 | + | ||
| 66 | +/** | ||
| 67 | + * 从 OpenAPI 文档提取 API 信息 | ||
| 68 | + */ | ||
| 69 | +function extractAPIInfo(openapiDoc) { | ||
| 70 | + const path = Object.keys(openapiDoc.paths)[0] | ||
| 71 | + const method = Object.keys(openapiDoc.paths[path])[0] | ||
| 72 | + const apiInfo = openapiDoc.paths[path][method] | ||
| 73 | + | ||
| 74 | + // 提取参数 | ||
| 75 | + const queryParams = (apiInfo.parameters || []) | ||
| 76 | + .filter(p => p.in === 'query' && p.name !== 'a' && p.name !== 'f') | ||
| 77 | + .map(p => ({ | ||
| 78 | + name: p.name, | ||
| 79 | + type: p.schema?.type || 'any', | ||
| 80 | + required: p.required || false, | ||
| 81 | + description: p.description || '', | ||
| 82 | + })) | ||
| 83 | + | ||
| 84 | + // 提取 body 参数 | ||
| 85 | + const bodyParams = [] | ||
| 86 | + if (apiInfo.requestBody && apiInfo.requestBody.content) { | ||
| 87 | + const content = | ||
| 88 | + apiInfo.requestBody.content['application/x-www-form-urlencoded'] || | ||
| 89 | + apiInfo.requestBody.content['application/json'] | ||
| 90 | + if (content && content.schema && content.schema.properties) { | ||
| 91 | + Object.entries(content.schema.properties).forEach(([key, value]) => { | ||
| 92 | + if (key !== 'a' && key !== 'f') { | ||
| 93 | + bodyParams.push({ | ||
| 94 | + name: key, | ||
| 95 | + type: value.type || 'any', | ||
| 96 | + required: content.schema.required?.includes(key) || false, | ||
| 97 | + description: value.description || '', | ||
| 98 | + }) | ||
| 99 | + } | ||
| 100 | + }) | ||
| 101 | + } | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + // 提取响应结构 | ||
| 105 | + const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema | ||
| 106 | + | ||
| 107 | + return { | ||
| 108 | + name: openapiDoc._fileName || 'unknown', | ||
| 109 | + path, | ||
| 110 | + method: method.toUpperCase(), | ||
| 111 | + queryParams: new Set(queryParams.map(p => p.name)), | ||
| 112 | + bodyParams: new Set(bodyParams.map(p => p.name)), | ||
| 113 | + requiredQueryParams: new Set(queryParams.filter(p => p.required).map(p => p.name)), | ||
| 114 | + requiredBodyParams: new Set(bodyParams.filter(p => p.required).map(p => p.name)), | ||
| 115 | + allQueryParams: queryParams, | ||
| 116 | + allBodyParams: bodyParams, | ||
| 117 | + responseSchema, | ||
| 118 | + summary: apiInfo.summary || '', | ||
| 119 | + } | ||
| 120 | +} | ||
| 121 | + | ||
| 122 | +/** | ||
| 123 | + * 对比两个 API 信息 | ||
| 124 | + */ | ||
| 125 | +function compareAPI(oldAPI, newAPI) { | ||
| 126 | + const changes = { | ||
| 127 | + breaking: [], | ||
| 128 | + nonBreaking: [], | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + // 检查 HTTP 方法变更 | ||
| 132 | + if (oldAPI.method !== newAPI.method) { | ||
| 133 | + changes.breaking.push(`HTTP 方法变更: ${oldAPI.method} → ${newAPI.method}`) | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + // 检查 GET 参数变更 | ||
| 137 | + oldAPI.allQueryParams.forEach(oldParam => { | ||
| 138 | + const newParam = newAPI.allQueryParams.find(p => p.name === oldParam.name) | ||
| 139 | + | ||
| 140 | + if (!newParam) { | ||
| 141 | + // 参数被删除 | ||
| 142 | + if (oldAPI.requiredQueryParams.has(oldParam.name)) { | ||
| 143 | + changes.breaking.push(`删除必填 query 参数: ${oldParam.name}`) | ||
| 144 | + } else { | ||
| 145 | + changes.nonBreaking.push(`删除可选 query 参数: ${oldParam.name}`) | ||
| 146 | + } | ||
| 147 | + } else { | ||
| 148 | + // 参数类型变更 | ||
| 149 | + if (oldParam.type !== newParam.type) { | ||
| 150 | + changes.breaking.push( | ||
| 151 | + `query 参数类型变更: ${oldParam.name} (${oldParam.type} → ${newParam.type})` | ||
| 152 | + ) | ||
| 153 | + } | ||
| 154 | + // 可选 → 必填 | ||
| 155 | + if (!oldParam.required && newParam.required) { | ||
| 156 | + changes.breaking.push(`query 参数变为必填: ${newParam.name}`) | ||
| 157 | + } | ||
| 158 | + // 必填 → 可选 | ||
| 159 | + if (oldParam.required && !newParam.required) { | ||
| 160 | + changes.nonBreaking.push(`query 参数变为可选: ${newParam.name}`) | ||
| 161 | + } | ||
| 162 | + } | ||
| 163 | + }) | ||
| 164 | + | ||
| 165 | + // 检查新增 GET 参数 | ||
| 166 | + newAPI.allQueryParams.forEach(newParam => { | ||
| 167 | + const oldParam = oldAPI.allQueryParams.find(p => p.name === newParam.name) | ||
| 168 | + if (!oldParam) { | ||
| 169 | + if (newParam.required) { | ||
| 170 | + changes.breaking.push(`新增必填 query 参数: ${newParam.name}`) | ||
| 171 | + } else { | ||
| 172 | + changes.nonBreaking.push(`新增可选 query 参数: ${newParam.name}`) | ||
| 173 | + } | ||
| 174 | + } | ||
| 175 | + }) | ||
| 176 | + | ||
| 177 | + // 检查 POST body 参数变更 | ||
| 178 | + oldAPI.allBodyParams.forEach(oldParam => { | ||
| 179 | + const newParam = newAPI.allBodyParams.find(p => p.name === oldParam.name) | ||
| 180 | + | ||
| 181 | + if (!newParam) { | ||
| 182 | + // 参数被删除 | ||
| 183 | + if (oldAPI.requiredBodyParams.has(oldParam.name)) { | ||
| 184 | + changes.breaking.push(`删除必填 body 参数: ${oldParam.name}`) | ||
| 185 | + } else { | ||
| 186 | + changes.nonBreaking.push(`删除可选 body 参数: ${oldParam.name}`) | ||
| 187 | + } | ||
| 188 | + } else { | ||
| 189 | + // 参数类型变更 | ||
| 190 | + if (oldParam.type !== newParam.type) { | ||
| 191 | + changes.breaking.push( | ||
| 192 | + `body 参数类型变更: ${oldParam.name} (${oldParam.type} → ${newParam.type})` | ||
| 193 | + ) | ||
| 194 | + } | ||
| 195 | + // 可选 → 必填 | ||
| 196 | + if (!oldParam.required && newParam.required) { | ||
| 197 | + changes.breaking.push(`body 参数变为必填: ${newParam.name}`) | ||
| 198 | + } | ||
| 199 | + // 必填 → 可选 | ||
| 200 | + if (oldParam.required && !newParam.required) { | ||
| 201 | + changes.nonBreaking.push(`body 参数变为可选: ${newParam.name}`) | ||
| 202 | + } | ||
| 203 | + } | ||
| 204 | + }) | ||
| 205 | + | ||
| 206 | + // 检查新增 body 参数 | ||
| 207 | + newAPI.allBodyParams.forEach(newParam => { | ||
| 208 | + const oldParam = oldAPI.allBodyParams.find(p => p.name === newParam.name) | ||
| 209 | + if (!oldParam) { | ||
| 210 | + if (newParam.required) { | ||
| 211 | + changes.breaking.push(`新增必填 body 参数: ${newParam.name}`) | ||
| 212 | + } else { | ||
| 213 | + changes.nonBreaking.push(`新增可选 body 参数: ${newParam.name}`) | ||
| 214 | + } | ||
| 215 | + } | ||
| 216 | + }) | ||
| 217 | + | ||
| 218 | + return changes | ||
| 219 | +} | ||
| 220 | + | ||
| 221 | +/** | ||
| 222 | + * 生成变更报告 | ||
| 223 | + */ | ||
| 224 | +function generateReport(oldDocs, newDocs, format = 'text') { | ||
| 225 | + const oldAPIs = oldDocs.map(extractAPIInfo) | ||
| 226 | + const newAPIs = newDocs.map(extractAPIInfo) | ||
| 227 | + | ||
| 228 | + const oldAPIsMap = new Map(oldAPIs.map(api => [api.name, api])) | ||
| 229 | + const newAPIsMap = new Map(newAPIs.map(api => [api.name, api])) | ||
| 230 | + | ||
| 231 | + const addedAPIs = [] | ||
| 232 | + const removedAPIs = [] | ||
| 233 | + const modifiedAPIs = [] | ||
| 234 | + | ||
| 235 | + // 检测新增接口 | ||
| 236 | + newAPIs.forEach(api => { | ||
| 237 | + if (!oldAPIsMap.has(api.name)) { | ||
| 238 | + addedAPIs.push(api) | ||
| 239 | + } | ||
| 240 | + }) | ||
| 241 | + | ||
| 242 | + // 检测删除接口 | ||
| 243 | + oldAPIs.forEach(api => { | ||
| 244 | + if (!newAPIsMap.has(api.name)) { | ||
| 245 | + removedAPIs.push(api) | ||
| 246 | + } | ||
| 247 | + }) | ||
| 248 | + | ||
| 249 | + // 检测修改接口 | ||
| 250 | + newAPIs.forEach(api => { | ||
| 251 | + const oldAPI = oldAPIsMap.get(api.name) | ||
| 252 | + if (oldAPI) { | ||
| 253 | + const changes = compareAPI(oldAPI, api) | ||
| 254 | + if (changes.breaking.length > 0 || changes.nonBreaking.length > 0) { | ||
| 255 | + modifiedAPIs.push({ | ||
| 256 | + name: api.name, | ||
| 257 | + summary: api.summary, | ||
| 258 | + changes, | ||
| 259 | + }) | ||
| 260 | + } | ||
| 261 | + } | ||
| 262 | + }) | ||
| 263 | + | ||
| 264 | + // 统计 | ||
| 265 | + const totalBreaking = modifiedAPIs.reduce((sum, api) => sum + api.changes.breaking.length, 0) | ||
| 266 | + | ||
| 267 | + // 生成文本报告 | ||
| 268 | + if (format === 'text') { | ||
| 269 | + const lines = [] | ||
| 270 | + lines.push('=== API 变更检测报告 ===\n') | ||
| 271 | + lines.push(`📦 对比范围: ${oldAPIs.length} 个旧接口 → ${newAPIs.length} 个新接口`) | ||
| 272 | + lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n') | ||
| 273 | + | ||
| 274 | + if (addedAPIs.length > 0) { | ||
| 275 | + lines.push(`✅ 新增接口 (${addedAPIs.length}):`) | ||
| 276 | + addedAPIs.forEach(api => { | ||
| 277 | + lines.push(` + ${api.name} - ${api.summary}`) | ||
| 278 | + }) | ||
| 279 | + lines.push('') | ||
| 280 | + } | ||
| 281 | + | ||
| 282 | + if (modifiedAPIs.length > 0) { | ||
| 283 | + lines.push(`⚠️ 修改接口 (${modifiedAPIs.length}):`) | ||
| 284 | + modifiedAPIs.forEach(api => { | ||
| 285 | + lines.push(` ↪ ${api.name} - ${api.summary}`) | ||
| 286 | + api.changes.breaking.forEach(change => { | ||
| 287 | + lines.push(` ✗ [破坏性] ${change}`) | ||
| 288 | + }) | ||
| 289 | + api.changes.nonBreaking.forEach(change => { | ||
| 290 | + lines.push(` ✓ [非破坏性] ${change}`) | ||
| 291 | + }) | ||
| 292 | + }) | ||
| 293 | + lines.push('') | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + if (removedAPIs.length > 0) { | ||
| 297 | + lines.push(`❌ 删除接口 (${removedAPIs.length}):`) | ||
| 298 | + removedAPIs.forEach(api => { | ||
| 299 | + lines.push(` - ${api.name} - ${api.summary}`) | ||
| 300 | + }) | ||
| 301 | + lines.push('') | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') | ||
| 305 | + lines.push( | ||
| 306 | + `总计: ${addedAPIs.length} 新增, ${modifiedAPIs.length} 修改, ${removedAPIs.length} 删除` | ||
| 307 | + ) | ||
| 308 | + | ||
| 309 | + if (totalBreaking > 0) { | ||
| 310 | + lines.push(`⚠️ 检测到 ${totalBreaking} 个破坏性变更,请仔细检查业务逻辑!`) | ||
| 311 | + } else if (addedAPIs.length > 0 || modifiedAPIs.length > 0 || removedAPIs.length > 0) { | ||
| 312 | + lines.push('✅ 未检测到破坏性变更') | ||
| 313 | + } else { | ||
| 314 | + lines.push('✅ 无接口变更') | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + return lines.join('\n') | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + // 生成 JSON 报告 | ||
| 321 | + if (format === 'json') { | ||
| 322 | + return JSON.stringify( | ||
| 323 | + { | ||
| 324 | + summary: { | ||
| 325 | + added: addedAPIs.length, | ||
| 326 | + modified: modifiedAPIs.length, | ||
| 327 | + removed: removedAPIs.length, | ||
| 328 | + breakingChanges: totalBreaking, | ||
| 329 | + }, | ||
| 330 | + added: addedAPIs.map(api => ({ | ||
| 331 | + name: api.name, | ||
| 332 | + summary: api.summary, | ||
| 333 | + method: api.method, | ||
| 334 | + path: api.path, | ||
| 335 | + })), | ||
| 336 | + modified: modifiedAPIs.map(api => ({ | ||
| 337 | + name: api.name, | ||
| 338 | + summary: api.summary, | ||
| 339 | + breakingChanges: api.changes.breaking, | ||
| 340 | + nonBreakingChanges: api.changes.nonBreaking, | ||
| 341 | + })), | ||
| 342 | + removed: removedAPIs.map(api => ({ | ||
| 343 | + name: api.name, | ||
| 344 | + summary: api.summary, | ||
| 345 | + method: api.method, | ||
| 346 | + path: api.path, | ||
| 347 | + })), | ||
| 348 | + }, | ||
| 349 | + null, | ||
| 350 | + 2 | ||
| 351 | + ) | ||
| 352 | + } | ||
| 353 | +} | ||
| 354 | + | ||
| 355 | +/** | ||
| 356 | + * 主函数 | ||
| 357 | + */ | ||
| 358 | +function main() { | ||
| 359 | + const args = process.argv.slice(2) | ||
| 360 | + | ||
| 361 | + if (args.length < 2) { | ||
| 362 | + console.error('用法: node scripts/apiDiff.js <oldPath> <newPath>') | ||
| 363 | + console.error('示例:') | ||
| 364 | + console.error(' node scripts/apiDiff.js docs/api-specs/user/ docs/api-specs/user-new/') | ||
| 365 | + console.error( | ||
| 366 | + ' node scripts/apiDiff.js docs/api-specs/user/api1.md docs/api-specs/user/api1-new.md' | ||
| 367 | + ) | ||
| 368 | + process.exit(1) | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + const [oldPath, newPath] = args | ||
| 372 | + | ||
| 373 | + if (!fs.existsSync(oldPath)) { | ||
| 374 | + console.error(`❌ 旧路径不存在: ${oldPath}`) | ||
| 375 | + process.exit(1) | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + if (!fs.existsSync(newPath)) { | ||
| 379 | + console.error(`❌ 新路径不存在: ${newPath}`) | ||
| 380 | + process.exit(1) | ||
| 381 | + } | ||
| 382 | + | ||
| 383 | + try { | ||
| 384 | + const oldDocs = parseOpenAPIPath(oldPath) | ||
| 385 | + const newDocs = parseOpenAPIPath(newPath) | ||
| 386 | + | ||
| 387 | + const format = process.env.API_DIFF_FORMAT || 'text' | ||
| 388 | + const report = generateReport(oldDocs, newDocs, format) | ||
| 389 | + | ||
| 390 | + console.log(report) | ||
| 391 | + | ||
| 392 | + // 如果有破坏性变更,返回退出码 1 | ||
| 393 | + const oldAPIs = oldDocs.map(extractAPIInfo) | ||
| 394 | + const newAPIs = newDocs.map(extractAPIInfo) | ||
| 395 | + const oldAPIsMap = new Map(oldAPIs.map(api => [api.name, api])) | ||
| 396 | + const newAPIsMap = new Map(newAPIs.map(api => [api.name, api])) | ||
| 397 | + | ||
| 398 | + let totalBreaking = 0 | ||
| 399 | + newAPIs.forEach(api => { | ||
| 400 | + const oldAPI = oldAPIsMap.get(api.name) | ||
| 401 | + if (oldAPI) { | ||
| 402 | + const changes = compareAPI(oldAPI, api) | ||
| 403 | + totalBreaking += changes.breaking.length | ||
| 404 | + } | ||
| 405 | + }) | ||
| 406 | + | ||
| 407 | + // 严格模式:任何变更都返回 1 | ||
| 408 | + const strictMode = process.env.API_DIFF_STRICT === 'true' | ||
| 409 | + const hasChanges = | ||
| 410 | + oldAPIs.length !== newAPIs.length || | ||
| 411 | + newAPIs.some(api => !oldAPIsMap.has(api.name)) || | ||
| 412 | + oldAPIs.some(api => !newAPIsMap.has(api.name)) | ||
| 413 | + | ||
| 414 | + if (totalBreaking > 0 || (strictMode && hasChanges)) { | ||
| 415 | + process.exit(1) | ||
| 416 | + } else { | ||
| 417 | + process.exit(0) | ||
| 418 | + } | ||
| 419 | + } catch (error) { | ||
| 420 | + console.error(`❌ 对比失败: ${error.message}`) | ||
| 421 | + process.exit(1) | ||
| 422 | + } | ||
| 423 | +} | ||
| 424 | + | ||
| 425 | +// 如果直接运行此脚本 | ||
| 426 | +if (require.main === module) { | ||
| 427 | + main() | ||
| 428 | +} | ||
| 429 | + | ||
| 430 | +module.exports = { | ||
| 431 | + compareAPI, | ||
| 432 | + generateReport, | ||
| 433 | + parseOpenAPIPath, | ||
| 434 | + extractAPIInfo, | ||
| 435 | +} |
scripts/check-changelog.sh
0 → 100755
| 1 | +#!/bin/bash | ||
| 2 | + | ||
| 3 | +############################################################################### | ||
| 4 | +# CHANGELOG 漏记检查脚本 | ||
| 5 | +# | ||
| 6 | +# 功能: | ||
| 7 | +# 1. 扫描最近 N 天的 git 提交记录 | ||
| 8 | +# 2. 对比 CHANGELOG.md 中的记录 | ||
| 9 | +# 3. 生成漏记报告 | ||
| 10 | +# | ||
| 11 | +# 使用: | ||
| 12 | +# ./scripts/check-changelog.sh [days] | ||
| 13 | +# | ||
| 14 | +# 示例: | ||
| 15 | +# ./scripts/check-changelog.sh 7 # 检查最近 7 天 | ||
| 16 | +# ./scripts/check-changelog.sh 30 # 检查最近 30 天 | ||
| 17 | +# ./scripts/check-changelog.sh # 检查所有提交 | ||
| 18 | +# | ||
| 19 | +############################################################################### | ||
| 20 | + | ||
| 21 | +set -e | ||
| 22 | + | ||
| 23 | +# 颜色定义 | ||
| 24 | +RED='\033[0;31m' | ||
| 25 | +GREEN='\033[0;32m' | ||
| 26 | +YELLOW='\033[1;33m' | ||
| 27 | +BLUE='\033[0;34m' | ||
| 28 | +NC='\033[0m' # No Color | ||
| 29 | + | ||
| 30 | +# 默认参数 | ||
| 31 | +DAYS=${1:-7} # 默认检查最近 7 天 | ||
| 32 | +CHANGELOG_FILE="docs/CHANGELOG.md" | ||
| 33 | + | ||
| 34 | +echo -e "${BLUE}======================================${NC}" | ||
| 35 | +echo -e "${BLUE} CHANGELOG 漏记检查工具${NC}" | ||
| 36 | +echo -e "${BLUE}======================================${NC}" | ||
| 37 | +echo "" | ||
| 38 | +echo -e "检查范围: 最近 ${DAYS} 天" | ||
| 39 | +echo "" | ||
| 40 | + | ||
| 41 | +# 检查 CHANGELOG 文件是否存在 | ||
| 42 | +if [ ! -f "$CHANGELOG_FILE" ]; then | ||
| 43 | + echo -e "${RED}错误: CHANGELOG 文件不存在: $CHANGELOG_FILE${NC}" | ||
| 44 | + exit 1 | ||
| 45 | +fi | ||
| 46 | + | ||
| 47 | +# 1. 获取 git 提交记录 | ||
| 48 | +echo -e "${BLUE}[1/4] 正在获取 git 提交记录...${NC}" | ||
| 49 | + | ||
| 50 | +if [ "$DAYS" = "0" ]; then | ||
| 51 | + # 检查所有提交 | ||
| 52 | + GIT_LOG=$(git log --all --pretty=format:"%h|%ad|%s" --date=short) | ||
| 53 | +else | ||
| 54 | + # 检查最近 N 天的提交 | ||
| 55 | + GIT_LOG=$(git log --since="$DAYS days ago" --pretty=format:"%h|%ad|%s" --date=short) | ||
| 56 | +fi | ||
| 57 | + | ||
| 58 | +TOTAL_COMMITS=$(echo "$GIT_LOG" | wc -l | tr -d ' ') | ||
| 59 | +echo -e " 找到 ${GREEN}$TOTAL_COMMITS${NC} 个提交" | ||
| 60 | + | ||
| 61 | +# 2. 解析 CHANGELOG 中的记录 | ||
| 62 | +echo -e "${BLUE}[2/4] 正在解析 CHANGELOG 记录...${NC}" | ||
| 63 | + | ||
| 64 | +# 提取 CHANGELOG 中的日期和描述 | ||
| 65 | +CHANGELOG_ENTRIES=$(grep "^## \[" "$CHANGELOG_FILE" | sed 's/^## \[//' | sed 's/\].*//' | sort -u) | ||
| 66 | +TOTAL_CHANGELOG=$(echo "$CHANGELOG_ENTRIES" | wc -l | tr -d ' ') | ||
| 67 | +echo -e " 找到 ${GREEN}$TOTAL_CHANGELOG${NC} 条记录" | ||
| 68 | + | ||
| 69 | +# 3. 对比分析 | ||
| 70 | +echo -e "${BLUE}[3/4] 正在对比分析...${NC}" | ||
| 71 | + | ||
| 72 | +# 统计每个日期的提交数量 | ||
| 73 | +COMMITS_BY_DATE=$(echo "$GIT_LOG" | awk -F'|' '{print $2}' | sort | uniq -c | sort -rn) | ||
| 74 | + | ||
| 75 | +echo "" | ||
| 76 | +echo -e "${YELLOW}📊 每日提交统计:${NC}" | ||
| 77 | +echo "$COMMITS_BY_DATE" | head -20 | ||
| 78 | + | ||
| 79 | +# 检查哪些日期有提交但 CHANGELOG 没有记录 | ||
| 80 | +echo "" | ||
| 81 | +echo -e "${YELLOW}🔍 可能漏记的日期:${NC}" | ||
| 82 | + | ||
| 83 | +MISSING_DATES=0 | ||
| 84 | +while IFS='|' read -r count date; do | ||
| 85 | + date=$(echo "$date" | awk '{print $2}') | ||
| 86 | + # 检查 CHANGELOG 中是否有这个日期的记录 | ||
| 87 | + if ! echo "$CHANGELOG_ENTRIES" | grep -q "$date"; then | ||
| 88 | + echo -e " ${RED}✗${NC} $date - ${RED}$count 个提交未记录${NC}" | ||
| 89 | + MISSING_DATES=$((MISSING_DATES + 1)) | ||
| 90 | + fi | ||
| 91 | +done <<< "$COMMITS_BY_DATE" | ||
| 92 | + | ||
| 93 | +if [ $MISSING_DATES -eq 0 ]; then | ||
| 94 | + echo -e " ${GREEN}✓${NC} 所有提交都已记录" | ||
| 95 | +fi | ||
| 96 | + | ||
| 97 | +# 4. 生成详细报告 | ||
| 98 | +echo "" | ||
| 99 | +echo -e "${BLUE}[4/4] 生成详细报告...${NC}" | ||
| 100 | + | ||
| 101 | +# 临时文件 | ||
| 102 | +TEMP_REPORT=$(mktemp) | ||
| 103 | + | ||
| 104 | +# 输出报告头 | ||
| 105 | +cat > "$TEMP_REPORT" << 'EOF' | ||
| 106 | +# CHANGELOG 漏记详细报告 | ||
| 107 | + | ||
| 108 | +## 检查日期 | ||
| 109 | +- 检查范围: 最近 {DAYS} 天 | ||
| 110 | +- 生成时间: {TIMESTAMP} | ||
| 111 | + | ||
| 112 | +## 统计摘要 | ||
| 113 | +- Git 提交总数: {TOTAL_COMMITS} | ||
| 114 | +- CHANGELOG 记录数: {TOTAL_CHANGELOG} | ||
| 115 | +- 可能漏记的提交: {MISSING_COMMITS} | ||
| 116 | + | ||
| 117 | +## 漏记详情 | ||
| 118 | + | ||
| 119 | +EOF | ||
| 120 | + | ||
| 121 | +# 替换模板变量 | ||
| 122 | +sed -i.bak "s/{DAYS}/$DAYS/g" "$TEMP_REPORT" | ||
| 123 | +sed -i.bak "s/{TIMESTAMP}/$(date '+%Y-%m-%d %H:%M:%S')/g" "$TEMP_REPORT" | ||
| 124 | +sed -i.bak "s/{TOTAL_COMMITS}/$TOTAL_COMMITS/g" "$TEMP_REPORT" | ||
| 125 | +sed -i.bak "s/{TOTAL_CHANGELOG}/$TOTAL_CHANGELOG/g" "$TEMP_REPORT" | ||
| 126 | + | ||
| 127 | +# 计算可能漏记的提交数 | ||
| 128 | +MISSING_COMMITS=0 | ||
| 129 | +while IFS='|' read -r count date; do | ||
| 130 | + date=$(echo "$date" | awk '{print $2}') | ||
| 131 | + if ! echo "$CHANGELOG_ENTRIES" | grep -q "$date"; then | ||
| 132 | + MISSING_COMMITS=$((MISSING_COMMITS + count)) | ||
| 133 | + fi | ||
| 134 | +done <<< "$COMMITS_BY_DATE" | ||
| 135 | + | ||
| 136 | +sed -i.bak "s/{MISSING_COMMITS}/$MISSING_COMMITS/g" "$TEMP_REPORT" | ||
| 137 | + | ||
| 138 | +# 如果有漏记,列出详细提交 | ||
| 139 | +if [ $MISSING_COMMITS -gt 0 ]; then | ||
| 140 | + echo "" >> "$TEMP_REPORT" | ||
| 141 | + echo "### 未记录的提交详情" >> "$TEMP_REPORT" | ||
| 142 | + echo "" >> "$TEMP_REPORT" | ||
| 143 | + | ||
| 144 | + while IFS='|' read -r count date; do | ||
| 145 | + date_only=$(echo "$date" | awk '{print $2}') | ||
| 146 | + if ! echo "$CHANGELOG_ENTRIES" | grep -q "$date_only"; then | ||
| 147 | + echo "**$date_only** ($count 个提交):" >> "$TEMP_REPORT" | ||
| 148 | + echo "$GIT_LOG" | grep "$date_only" | awk -F'|' '{print "- " $3}' >> "$TEMP_REPORT" | ||
| 149 | + echo "" >> "$TEMP_REPORT" | ||
| 150 | + fi | ||
| 151 | + done <<< "$COMMITS_BY_DATE" | ||
| 152 | +else | ||
| 153 | + echo "" >> "$TEMP_REPORT" | ||
| 154 | + echo "### ✅ 完整性检查" >> "$TEMP_REPORT" | ||
| 155 | + echo "" >> "$TEMP_REPORT" | ||
| 156 | + echo "所有提交都已在 CHANGELOG 中记录!" >> "$TEMP_REPORT" | ||
| 157 | +fi | ||
| 158 | + | ||
| 159 | +# 删除备份文件 | ||
| 160 | +rm -f "$TEMP_REPORT.bak" | ||
| 161 | + | ||
| 162 | +# 输出报告 | ||
| 163 | +echo "" | ||
| 164 | +echo -e "${BLUE}======================================${NC}" | ||
| 165 | +echo -e "${BLUE} 检查完成${NC}" | ||
| 166 | +echo -e "${BLUE}======================================${NC}" | ||
| 167 | +echo "" | ||
| 168 | +cat "$TEMP_REPORT" | ||
| 169 | + | ||
| 170 | +# 保存报告 | ||
| 171 | +REPORT_FILE="docs/changelog-check-report-$(date +%Y%m%d).md" | ||
| 172 | +mv "$TEMP_REPORT" "$REPORT_FILE" | ||
| 173 | + | ||
| 174 | +echo "" | ||
| 175 | +echo -e "${GREEN}✓ 详细报告已保存到: $REPORT_FILE${NC}" | ||
| 176 | + | ||
| 177 | +# 5. 给出建议 | ||
| 178 | +echo "" | ||
| 179 | +echo -e "${YELLOW}💡 建议:${NC}" | ||
| 180 | +if [ $MISSING_COMMITS -gt 0 ]; then | ||
| 181 | + echo -e " ${YELLOW}1.${NC} 查看 $REPORT_FILE 了解漏记详情" | ||
| 182 | + echo -e " ${YELLOW}2.${NC} 更新 CHANGELOG.md 补充漏记的记录" | ||
| 183 | + echo -e " ${YELLOW}3.${NC} 使用标准格式添加记录(参考文档顶部模板)" | ||
| 184 | +else | ||
| 185 | + echo -e " ${GREEN}✓${NC} CHANGELOG 记录完整,继续保持!" | ||
| 186 | +fi | ||
| 187 | + | ||
| 188 | +echo "" | ||
| 189 | +echo -e "${BLUE}======================================${NC}" | ||
| 190 | +echo "" | ||
| 191 | + | ||
| 192 | +# 返回退出码 | ||
| 193 | +if [ $MISSING_COMMITS -gt 0 ]; then | ||
| 194 | + exit 1 # 有漏记,返回非零退出码 | ||
| 195 | +else | ||
| 196 | + exit 0 # 无漏记,返回零 | ||
| 197 | +fi |
scripts/generateApiFromOpenAPI.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 从 OpenAPI 文档自动生成 API 接口文件 | ||
| 3 | + * | ||
| 4 | + * 功能: | ||
| 5 | + * 1. 扫描 docs/api-specs 目录 | ||
| 6 | + * 2. 解析每个 .md 文件中的 OpenAPI YAML 规范 | ||
| 7 | + * 3. 提取 API 信息并生成对应的 JavaScript API 文件 | ||
| 8 | + * 4. 保存到 src/api/ 目录 | ||
| 9 | + * | ||
| 10 | + * 目录结构: | ||
| 11 | + * docs/api-specs/ | ||
| 12 | + * ├── module1/ | ||
| 13 | + * │ ├── api1.md | ||
| 14 | + * │ └── api2.md | ||
| 15 | + * └── module2/ | ||
| 16 | + * └── api3.md | ||
| 17 | + * | ||
| 18 | + * 生成到: | ||
| 19 | + * src/api/ | ||
| 20 | + * ├── module1.js | ||
| 21 | + * └── module2.js | ||
| 22 | + */ | ||
| 23 | + | ||
| 24 | +const fs = require('fs') | ||
| 25 | +const path = require('path') | ||
| 26 | +const yaml = require('js-yaml') | ||
| 27 | +const { generateReport, parseOpenAPIPath } = require('./apiDiff') | ||
| 28 | + | ||
| 29 | +/** | ||
| 30 | + * 提取 Markdown 文件中的 YAML 代码块 | ||
| 31 | + * @param {string} content - Markdown 文件内容 | ||
| 32 | + * @returns {string|null} - YAML 字符串或 null | ||
| 33 | + */ | ||
| 34 | +function extractYAMLFromMarkdown(content) { | ||
| 35 | + const yamlRegex = /```yaml\s*\n([\s\S]*?)\n```/ | ||
| 36 | + const match = content.match(yamlRegex) | ||
| 37 | + return match ? match[1] : null | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +/** | ||
| 41 | + * 将字符串转换为驼峰命名 | ||
| 42 | + * @param {string} str - 输入字符串 | ||
| 43 | + * @returns {string} - 驼峰命名字符串 | ||
| 44 | + */ | ||
| 45 | +function toCamelCase(str) { | ||
| 46 | + return str | ||
| 47 | + .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')) | ||
| 48 | + .replace(/^(.)/, c => c.toLowerCase()) | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +/** | ||
| 52 | + * 将字符串转换为帕斯卡命名(首字母大写) | ||
| 53 | + * @param {string} str - 输入字符串 | ||
| 54 | + * @returns {string} - 帕斯卡命名字符串 | ||
| 55 | + */ | ||
| 56 | +function toPascalCase(str) { | ||
| 57 | + const camelCase = toCamelCase(str) | ||
| 58 | + return camelCase.charAt(0).toUpperCase() + camelCase.slice(1) | ||
| 59 | +} | ||
| 60 | + | ||
| 61 | +/** | ||
| 62 | + * 简单的英文到中文翻译 | ||
| 63 | + * @param {string} text - 英文文本 | ||
| 64 | + * @returns {string} - 中文文本 | ||
| 65 | + */ | ||
| 66 | +function translateToChinese(text) { | ||
| 67 | + if (!text) { | ||
| 68 | + return '' | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + // 常见技术术语翻译字典(优先级从高到低) | ||
| 72 | + const dictionary = { | ||
| 73 | + // 短语和完整句子 | ||
| 74 | + 'mini program authorization': '小程序授权', | ||
| 75 | + 'login flow design': '登录流程设计', | ||
| 76 | + | ||
| 77 | + // 短语 | ||
| 78 | + 'mini program': '小程序', | ||
| 79 | + 'mini-program': '小程序', | ||
| 80 | + wechat: '微信', | ||
| 81 | + weixin: '微信', | ||
| 82 | + openid: 'OpenID', | ||
| 83 | + | ||
| 84 | + // 动词和常用词 | ||
| 85 | + authorize: '授权', | ||
| 86 | + authorization: '授权', | ||
| 87 | + login: '登录', | ||
| 88 | + logout: '登出', | ||
| 89 | + register: '注册', | ||
| 90 | + call: '调用', | ||
| 91 | + return: '返回', | ||
| 92 | + using: '使用', | ||
| 93 | + 'bound to': '绑定到', | ||
| 94 | + 'according to': '根据', | ||
| 95 | + | ||
| 96 | + // 名词 | ||
| 97 | + user: '用户', | ||
| 98 | + code: '授权码', | ||
| 99 | + token: '令牌', | ||
| 100 | + session: '会话', | ||
| 101 | + request: '请求', | ||
| 102 | + response: '响应', | ||
| 103 | + interface: '接口', | ||
| 104 | + api: '接口', | ||
| 105 | + account: '账号', | ||
| 106 | + openid: 'OpenID', | ||
| 107 | + | ||
| 108 | + // 描述性词汇 | ||
| 109 | + first: '首先', | ||
| 110 | + then: '然后', | ||
| 111 | + if: '如果', | ||
| 112 | + else: '否则', | ||
| 113 | + need: '需要', | ||
| 114 | + should: '应该', | ||
| 115 | + must: '必须', | ||
| 116 | + | ||
| 117 | + // 状态 | ||
| 118 | + success: '成功', | ||
| 119 | + fail: '失败', | ||
| 120 | + error: '错误', | ||
| 121 | + empty: '空', | ||
| 122 | + 'non-empty': '非空', | ||
| 123 | + | ||
| 124 | + // 属性 | ||
| 125 | + avatar: '头像', | ||
| 126 | + name: '姓名', | ||
| 127 | + id: 'ID', | ||
| 128 | + info: '信息', | ||
| 129 | + data: '数据', | ||
| 130 | + flow: '流程', | ||
| 131 | + design: '设计', | ||
| 132 | + | ||
| 133 | + // 其他 | ||
| 134 | + internal: '内部', | ||
| 135 | + automatically: '自动', | ||
| 136 | + specify: '指定', | ||
| 137 | + testing: '测试', | ||
| 138 | + 'used for': '用于', | ||
| 139 | + 'bound with': '绑定', | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + let translated = text | ||
| 143 | + | ||
| 144 | + // 按照字典进行替换(优先匹配长词) | ||
| 145 | + const sortedKeys = Object.keys(dictionary).sort((a, b) => b.length - a.length) | ||
| 146 | + sortedKeys.forEach(key => { | ||
| 147 | + const regex = new RegExp(key, 'gi') | ||
| 148 | + translated = translated.replace(regex, dictionary[key]) | ||
| 149 | + }) | ||
| 150 | + | ||
| 151 | + return translated | ||
| 152 | +} | ||
| 153 | + | ||
| 154 | +/** | ||
| 155 | + * 格式化描述文本(翻译+格式化) | ||
| 156 | + * @param {string} description - 原始描述 | ||
| 157 | + * @returns {string} - 格式化后的描述 | ||
| 158 | + */ | ||
| 159 | +function formatDescription(description) { | ||
| 160 | + if (!description) { | ||
| 161 | + return '' | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + // 移除 markdown 格式符号(如 # 标题) | ||
| 165 | + let formatted = description | ||
| 166 | + .replace(/^#+\s*/gm, '') // 移除标题符号 | ||
| 167 | + .replace(/\*\*(.*?)\*\*/g, '$1') // 移除加粗 | ||
| 168 | + .replace(/\*(.*?)\*/g, '$1') // 移除斜体 | ||
| 169 | + .replace(/`([^`]+)`/g, '$1') // 移除行内代码 | ||
| 170 | + .trim() | ||
| 171 | + | ||
| 172 | + // 先进行整句翻译(常见句式) | ||
| 173 | + formatted = translateSentences(formatted) | ||
| 174 | + | ||
| 175 | + return formatted | ||
| 176 | +} | ||
| 177 | + | ||
| 178 | +/** | ||
| 179 | + * 翻译常见句式 | ||
| 180 | + * @param {string} text - 文本 | ||
| 181 | + * @returns {string} - 翻译后的文本 | ||
| 182 | + */ | ||
| 183 | +function translateSentences(text) { | ||
| 184 | + if (!text) { | ||
| 185 | + return '' | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + // 常见句式翻译(按优先级排序,长的先匹配) | ||
| 189 | + const sentences = { | ||
| 190 | + // 完整句子 | ||
| 191 | + '# 登录流程设计': '# 登录流程设计', | ||
| 192 | + '# Login Flow Design': '# 登录流程设计', | ||
| 193 | + | ||
| 194 | + // 常见句式 | ||
| 195 | + 'Authorize mini program first': '先进行小程序授权', | ||
| 196 | + 'If user is empty, call login API': '如果返回 user 为空,则需要调用登录接口', | ||
| 197 | + 'If user is not empty, no need to call login API': '如果返回 user 非空,则不需要调用登录接口', | ||
| 198 | + 'the authorization API will automatically login using the account bound to openid': | ||
| 199 | + '授权接口内部按照 openid 绑定的账号,自动登录', | ||
| 200 | + 'Specify an openid for testing': '指定一个 openid 用来测试', | ||
| 201 | + 'User information bound to openid': 'openid 绑定的用户信息', | ||
| 202 | + '0=fail, 1=success': '0=失败,1=成功', | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + let translated = text | ||
| 206 | + | ||
| 207 | + // 按长度排序(长句优先) | ||
| 208 | + const sortedKeys = Object.keys(sentences).sort((a, b) => b.length - a.length) | ||
| 209 | + sortedKeys.forEach(key => { | ||
| 210 | + const regex = new RegExp(escapeRegExp(key), 'gi') | ||
| 211 | + translated = translated.replace(regex, sentences[key]) | ||
| 212 | + }) | ||
| 213 | + | ||
| 214 | + // 最后进行单词级别的补充翻译 | ||
| 215 | + translated = translateWords(translated) | ||
| 216 | + | ||
| 217 | + return translated | ||
| 218 | +} | ||
| 219 | + | ||
| 220 | +/** | ||
| 221 | + * 转义正则表达式特殊字符 | ||
| 222 | + * @param {string} string - 字符串 | ||
| 223 | + * @returns {string} - 转义后的字符串 | ||
| 224 | + */ | ||
| 225 | +function escapeRegExp(string) { | ||
| 226 | + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | ||
| 227 | +} | ||
| 228 | + | ||
| 229 | +/** | ||
| 230 | + * 单词级别的翻译(补充) | ||
| 231 | + * @param {string} text - 文本 | ||
| 232 | + * @returns {string} - 翻译后的文本 | ||
| 233 | + */ | ||
| 234 | +function translateWords(text) { | ||
| 235 | + const dictionary = { | ||
| 236 | + 'mini program': '小程序', | ||
| 237 | + wechat: '微信', | ||
| 238 | + openid: 'OpenID', | ||
| 239 | + user: '用户', | ||
| 240 | + authorization: '授权', | ||
| 241 | + login: '登录', | ||
| 242 | + avatar: '头像', | ||
| 243 | + name: '姓名', | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + let translated = text | ||
| 247 | + Object.entries(dictionary).forEach(([key, value]) => { | ||
| 248 | + const regex = new RegExp(key, 'gi') | ||
| 249 | + translated = translated.replace(regex, value) | ||
| 250 | + }) | ||
| 251 | + | ||
| 252 | + return translated | ||
| 253 | +} | ||
| 254 | + | ||
| 255 | +/** | ||
| 256 | + * 解析对象属性,生成字段描述 | ||
| 257 | + * @param {object} properties - 属性对象 | ||
| 258 | + * @param {number} indent - 缩进级别 | ||
| 259 | + * @returns {string} - 字段描述字符串 | ||
| 260 | + */ | ||
| 261 | +function parseProperties(properties, indent = 0) { | ||
| 262 | + if (!properties) { | ||
| 263 | + return '' | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + const lines = [] | ||
| 267 | + const prefix = ' '.repeat(indent) | ||
| 268 | + | ||
| 269 | + Object.entries(properties).forEach(([key, value]) => { | ||
| 270 | + const type = value.type || 'any' | ||
| 271 | + const desc = value.description || value.title || '' | ||
| 272 | + const required = value.required ? '' : ' (可选)' | ||
| 273 | + | ||
| 274 | + // 基本类型 | ||
| 275 | + if (type !== 'object' && type !== 'array') { | ||
| 276 | + lines.push(`${prefix}${key}: ${type}${required} - ${desc}`) | ||
| 277 | + // 对象类型 | ||
| 278 | + } else if (type === 'object' && value.properties) { | ||
| 279 | + lines.push(`${prefix}${key}: {`) | ||
| 280 | + lines.push(`${prefix} // ${desc}`) | ||
| 281 | + lines.push(parseProperties(value.properties, indent + 2)) | ||
| 282 | + lines.push(prefix + '}') | ||
| 283 | + // 数组类型 | ||
| 284 | + } else if (type === 'array' && value.items) { | ||
| 285 | + const itemType = value.items.type || 'any' | ||
| 286 | + if (itemType === 'object' && value.items.properties) { | ||
| 287 | + lines.push(`${prefix}${key}: Array<{`) | ||
| 288 | + lines.push(`${prefix} // ${desc}`) | ||
| 289 | + lines.push(parseProperties(value.items.properties, indent + 2)) | ||
| 290 | + lines.push(prefix + '}>') | ||
| 291 | + } else { | ||
| 292 | + lines.push(`${prefix}${key}: Array<${itemType}>${required} - ${desc}`) | ||
| 293 | + } | ||
| 294 | + } | ||
| 295 | + }) | ||
| 296 | + | ||
| 297 | + return lines.join('\n') | ||
| 298 | +} | ||
| 299 | + | ||
| 300 | +/** | ||
| 301 | + * 从 requestBody 中提取参数 | ||
| 302 | + * @param {object} requestBody - requestBody 对象 | ||
| 303 | + * @returns {Array} - 参数数组 | ||
| 304 | + */ | ||
| 305 | +function extractRequestParams(requestBody) { | ||
| 306 | + if (!requestBody || !requestBody.content) { | ||
| 307 | + return [] | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + // 获取内容类型(可能是 application/x-www-form-urlencoded 或 application/json) | ||
| 311 | + const content = | ||
| 312 | + requestBody.content['application/x-www-form-urlencoded'] || | ||
| 313 | + requestBody.content['application/json'] | ||
| 314 | + | ||
| 315 | + if (!content || !content.schema || !content.schema.properties) { | ||
| 316 | + return [] | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + const params = [] | ||
| 320 | + Object.entries(content.schema.properties).forEach(([key, value]) => { | ||
| 321 | + params.push({ | ||
| 322 | + name: key, | ||
| 323 | + type: value.type || 'any', | ||
| 324 | + description: value.description || '', | ||
| 325 | + example: value.example || '', | ||
| 326 | + required: content.schema.required?.includes(key) || false, | ||
| 327 | + }) | ||
| 328 | + }) | ||
| 329 | + | ||
| 330 | + return params | ||
| 331 | +} | ||
| 332 | + | ||
| 333 | +/** | ||
| 334 | + * 生成 JSDoc 参数注释 | ||
| 335 | + * @param {Array} parameters - parameters 数组(GET 请求) | ||
| 336 | + * @param {Array} bodyParams - requestBody 参数数组(POST 请求) | ||
| 337 | + * @param {string} method - HTTP 方法 | ||
| 338 | + * @returns {string} - JSDoc 参数注释 | ||
| 339 | + */ | ||
| 340 | +function generateParamJSDoc(parameters, bodyParams, method) { | ||
| 341 | + const lines = [' * @param {Object} params 请求参数'] | ||
| 342 | + | ||
| 343 | + // POST 请求使用 body 参数 | ||
| 344 | + if (method === 'POST' && bodyParams && bodyParams.length > 0) { | ||
| 345 | + // 过滤掉 a、f 和 t 参数(这些参数已硬编码到 URL 中) | ||
| 346 | + const filteredParams = bodyParams.filter( | ||
| 347 | + p => p.name !== 'a' && p.name !== 'f' && p.name !== 't' | ||
| 348 | + ) | ||
| 349 | + | ||
| 350 | + filteredParams.forEach(param => { | ||
| 351 | + const type = param.type || 'any' | ||
| 352 | + const desc = param.description || '' | ||
| 353 | + const required = param.required ? '' : ' (可选)' | ||
| 354 | + lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`) | ||
| 355 | + }) | ||
| 356 | + // GET 请求使用 query 参数 | ||
| 357 | + } else if (method === 'GET' && parameters && parameters.length > 0) { | ||
| 358 | + // 只保留 query 参数,过滤 header 参数和 a、f、t 参数 | ||
| 359 | + const queryParams = parameters.filter( | ||
| 360 | + p => p.in === 'query' && p.name !== 'a' && p.name !== 'f' && p.name !== 't' | ||
| 361 | + ) | ||
| 362 | + | ||
| 363 | + queryParams.forEach(param => { | ||
| 364 | + const type = param.schema?.type || 'any' | ||
| 365 | + const desc = param.description || '' | ||
| 366 | + const required = param.required ? '' : ' (可选)' | ||
| 367 | + lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`) | ||
| 368 | + }) | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + return lines.join('\n') | ||
| 372 | +} | ||
| 373 | + | ||
| 374 | +/** | ||
| 375 | + * 递归生成属性字段的 JSDoc 注释 | ||
| 376 | + * @param {object} properties - 属性对象 | ||
| 377 | + * @param {number} indent - 缩进级别(空格数) | ||
| 378 | + * @returns {string} - JSDoc 注释 | ||
| 379 | + */ | ||
| 380 | +function generatePropertiesJSDoc(properties, indent = 0) { | ||
| 381 | + const lines = [] | ||
| 382 | + const prefix = ' '.repeat(indent) | ||
| 383 | + | ||
| 384 | + Object.entries(properties).forEach(([key, value]) => { | ||
| 385 | + const type = value.type || 'any' | ||
| 386 | + const desc = value.description || value.title || '' | ||
| 387 | + | ||
| 388 | + // 处理嵌套对象 | ||
| 389 | + if (type === 'object' && value.properties) { | ||
| 390 | + lines.push(`${prefix}${key}: {\n`) | ||
| 391 | + // 递归处理嵌套对象的属性 | ||
| 392 | + lines.push(generatePropertiesJSDoc(value.properties, indent + 2)) | ||
| 393 | + lines.push(`${prefix}};\n`) | ||
| 394 | + // 处理数组(元素是对象) | ||
| 395 | + } else if (type === 'array' && value.items && value.items.properties) { | ||
| 396 | + lines.push(`${prefix}${key}: Array<{\n`) | ||
| 397 | + // 递归处理数组元素的属性 | ||
| 398 | + lines.push(generatePropertiesJSDoc(value.items.properties, indent + 2)) | ||
| 399 | + lines.push(`${prefix}}>;\n`) | ||
| 400 | + // 处理简单数组 | ||
| 401 | + } else if (type === 'array' && value.items) { | ||
| 402 | + const itemType = value.items.type || 'any' | ||
| 403 | + lines.push(`${prefix}${key}: Array<${itemType}>; // ${desc}\n`) | ||
| 404 | + // 处理基本类型 | ||
| 405 | + } else { | ||
| 406 | + lines.push(`${prefix}${key}: ${type}; // ${desc}\n`) | ||
| 407 | + } | ||
| 408 | + }) | ||
| 409 | + | ||
| 410 | + return lines.join('') | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +/** | ||
| 414 | + * 生成 JSDoc 返回值注释 | ||
| 415 | + * @param {object} responseSchema - 响应 schema | ||
| 416 | + * @returns {string} - JSDoc 返回值注释 | ||
| 417 | + */ | ||
| 418 | +function generateReturnJSDoc(responseSchema) { | ||
| 419 | + if (!responseSchema || !responseSchema.properties) { | ||
| 420 | + return ' * @returns {Promise<{code:number,data:any,msg:string}>} 标准返回' | ||
| 421 | + } | ||
| 422 | + | ||
| 423 | + const { code, msg, data } = responseSchema.properties | ||
| 424 | + | ||
| 425 | + let returnDesc = ' * @returns {Promise<{\n' | ||
| 426 | + returnDesc += ' * code: number; // 状态码\n' | ||
| 427 | + returnDesc += ' * msg: string; // 消息\n' | ||
| 428 | + | ||
| 429 | + if (data) { | ||
| 430 | + const dataType = data.type || 'any' | ||
| 431 | + const dataDesc = data.description || data.title || '' | ||
| 432 | + | ||
| 433 | + // 处理对象类型的 data | ||
| 434 | + if (dataType === 'object' && data.properties) { | ||
| 435 | + returnDesc += ' * data: {\n' | ||
| 436 | + // 使用递归函数处理 data 的所有属性 | ||
| 437 | + returnDesc += generatePropertiesJSDoc(data.properties, 4) | ||
| 438 | + returnDesc += ' * };\n' | ||
| 439 | + // 处理数组类型的 data(元素是对象) | ||
| 440 | + } else if (dataType === 'array' && data.items && data.items.properties) { | ||
| 441 | + returnDesc += ' * data: Array<{\n' | ||
| 442 | + returnDesc += generatePropertiesJSDoc(data.items.properties, 4) | ||
| 443 | + returnDesc += ' * }>;\n' | ||
| 444 | + // 处理简单数组类型 | ||
| 445 | + } else if (dataType === 'array' && data.items) { | ||
| 446 | + const itemType = data.items.type || 'any' | ||
| 447 | + returnDesc += ` * data: Array<${itemType}>;\n` | ||
| 448 | + // 其他类型 | ||
| 449 | + } else { | ||
| 450 | + returnDesc += ` * data: ${dataType};\n` | ||
| 451 | + } | ||
| 452 | + } else { | ||
| 453 | + returnDesc += ' * data: any;\n' | ||
| 454 | + } | ||
| 455 | + | ||
| 456 | + returnDesc += ' * }>}' | ||
| 457 | + | ||
| 458 | + return returnDesc | ||
| 459 | +} | ||
| 460 | + | ||
| 461 | +/** | ||
| 462 | + * 解析 OpenAPI 文档并提取 API 信息 | ||
| 463 | + * @param {object} openapiDoc - 解析后的 OpenAPI 对象 | ||
| 464 | + * @param {string} fileName - 文件名(用作 API 名称) | ||
| 465 | + * @returns {object} - 提取的 API 信息 | ||
| 466 | + */ | ||
| 467 | +function parseOpenAPIDocument(openapiDoc, fileName) { | ||
| 468 | + try { | ||
| 469 | + const path = Object.keys(openapiDoc.paths)[0] | ||
| 470 | + const method = Object.keys(openapiDoc.paths[path])[0] | ||
| 471 | + const apiInfo = openapiDoc.paths[path][method] | ||
| 472 | + | ||
| 473 | + // 提取 query 参数 | ||
| 474 | + const parameters = apiInfo.parameters || [] | ||
| 475 | + const queryParams = {} | ||
| 476 | + let actionValue = '' | ||
| 477 | + let typeValue = '' // t 参数 | ||
| 478 | + | ||
| 479 | + // 提取 body 参数(用于 POST 请求) | ||
| 480 | + const requestBody = apiInfo.requestBody | ||
| 481 | + const bodyParams = extractRequestParams(requestBody) | ||
| 482 | + | ||
| 483 | + // 对于 POST 请求,从 requestBody 中提取 action 和 type | ||
| 484 | + if (requestBody && bodyParams.length > 0) { | ||
| 485 | + const actionParam = bodyParams.find(p => p.name === 'a') | ||
| 486 | + if (actionParam) { | ||
| 487 | + actionValue = actionParam.example || '' | ||
| 488 | + } | ||
| 489 | + const typeParam = bodyParams.find(p => p.name === 't') | ||
| 490 | + if (typeParam) { | ||
| 491 | + typeValue = typeParam.example || '' | ||
| 492 | + } | ||
| 493 | + } | ||
| 494 | + | ||
| 495 | + // 对于 GET 请求,从 query 参数中提取 action 和 type | ||
| 496 | + if (parameters.length > 0) { | ||
| 497 | + parameters.forEach(param => { | ||
| 498 | + if (param.in === 'query') { | ||
| 499 | + queryParams[param.name] = param.example || param.schema?.default || '' | ||
| 500 | + | ||
| 501 | + // 提取 action 参数(通常是 'a' 参数) | ||
| 502 | + if (param.name === 'a' && !actionValue) { | ||
| 503 | + actionValue = param.example || '' | ||
| 504 | + } | ||
| 505 | + // 提取 type 参数('t' 参数) | ||
| 506 | + if (param.name === 't' && !typeValue) { | ||
| 507 | + typeValue = param.example || '' | ||
| 508 | + } | ||
| 509 | + } | ||
| 510 | + }) | ||
| 511 | + } | ||
| 512 | + | ||
| 513 | + // 提取响应结构 | ||
| 514 | + const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema | ||
| 515 | + | ||
| 516 | + return { | ||
| 517 | + summary: apiInfo.summary || fileName, | ||
| 518 | + description: apiInfo.description || '', | ||
| 519 | + method: method.toUpperCase(), | ||
| 520 | + action: actionValue, | ||
| 521 | + type: typeValue, // 保存 t 参数 | ||
| 522 | + queryParams, | ||
| 523 | + parameters, // 保存完整的参数信息用于生成 JSDoc(GET 请求) | ||
| 524 | + bodyParams, // 保存 requestBody 参数用于生成 JSDoc(POST 请求) | ||
| 525 | + responseSchema, // 保存响应结构用于生成 JSDoc | ||
| 526 | + fileName, | ||
| 527 | + } | ||
| 528 | + } catch (error) { | ||
| 529 | + console.error(`解析 OpenAPI 文档失败: ${error.message}`) | ||
| 530 | + return null | ||
| 531 | + } | ||
| 532 | +} | ||
| 533 | + | ||
| 534 | +/** | ||
| 535 | + * 生成 API 文件内容 | ||
| 536 | + * @param {string} moduleName - 模块名称 | ||
| 537 | + * @param {Array} apis - API 信息数组 | ||
| 538 | + * @returns {string} - 生成的文件内容 | ||
| 539 | + */ | ||
| 540 | +function generateApiFileContent(moduleName, apis) { | ||
| 541 | + const imports = "import { fn, fetch } from '@/api/fn';\n\n" | ||
| 542 | + const apiConstants = [] | ||
| 543 | + const apiFunctions = [] | ||
| 544 | + | ||
| 545 | + apis.forEach(api => { | ||
| 546 | + // 生成常量名(帕斯卡命名) | ||
| 547 | + const constantName = toPascalCase(api.fileName) | ||
| 548 | + // 生成函数名(驼峰命名 + API 后缀) | ||
| 549 | + const functionName = toCamelCase(api.fileName) + 'API' | ||
| 550 | + | ||
| 551 | + // 构建 URL,包含 a 和 t 参数 | ||
| 552 | + let url = '/srv/?' | ||
| 553 | + const params = [] | ||
| 554 | + if (api.action) { | ||
| 555 | + params.push(`a=${api.action}`) | ||
| 556 | + } | ||
| 557 | + if (api.type) { | ||
| 558 | + params.push(`t=${api.type}`) | ||
| 559 | + } | ||
| 560 | + url += params.join('&') | ||
| 561 | + | ||
| 562 | + // 添加常量定义 | ||
| 563 | + apiConstants.push(` ${constantName}: '${url}',`) | ||
| 564 | + | ||
| 565 | + // 生成详细的 JSDoc 注释 | ||
| 566 | + const paramJSDoc = generateParamJSDoc(api.parameters, api.bodyParams, api.method) | ||
| 567 | + const returnJSDoc = generateReturnJSDoc(api.responseSchema) | ||
| 568 | + | ||
| 569 | + // 添加函数定义 | ||
| 570 | + const fetchMethod = api.method === 'GET' ? 'fetch.get' : 'fetch.post' | ||
| 571 | + | ||
| 572 | + // 格式化描述 | ||
| 573 | + const formattedDesc = formatDescription(api.description) | ||
| 574 | + | ||
| 575 | + // 生成 JSDoc 注释(包含描述) | ||
| 576 | + const comment = `/** | ||
| 577 | + * @description ${api.summary} | ||
| 578 | + * @remark ${formattedDesc} | ||
| 579 | +${paramJSDoc} | ||
| 580 | +${returnJSDoc} | ||
| 581 | + */` | ||
| 582 | + | ||
| 583 | + apiFunctions.push( | ||
| 584 | + `${comment}\nexport const ${functionName} = (params) => fn(${fetchMethod}(Api.${constantName}, params));` | ||
| 585 | + ) | ||
| 586 | + }) | ||
| 587 | + | ||
| 588 | + return `${imports}const Api = {\n${apiConstants.join('\n')}\n}\n\n${apiFunctions.join('\n\n')}\n` | ||
| 589 | +} | ||
| 590 | + | ||
| 591 | +/** | ||
| 592 | + * 扫描目录并处理所有 OpenAPI 文档 | ||
| 593 | + * @param {string} openAPIDir - OpenAPI 文档目录 | ||
| 594 | + * @param {string} outputDir - 输出目录 | ||
| 595 | + */ | ||
| 596 | +function scanAndGenerate(openAPIDir, outputDir) { | ||
| 597 | + if (!fs.existsSync(openAPIDir)) { | ||
| 598 | + console.error(`OpenAPI 目录不存在: ${openAPIDir}`) | ||
| 599 | + return | ||
| 600 | + } | ||
| 601 | + | ||
| 602 | + // 确保输出目录存在 | ||
| 603 | + if (!fs.existsSync(outputDir)) { | ||
| 604 | + fs.mkdirSync(outputDir, { recursive: true }) | ||
| 605 | + } | ||
| 606 | + | ||
| 607 | + // 扫描第一级目录(模块) | ||
| 608 | + const modules = fs | ||
| 609 | + .readdirSync(openAPIDir, { withFileTypes: true }) | ||
| 610 | + .filter(dirent => dirent.isDirectory()) | ||
| 611 | + .map(dirent => dirent.name) | ||
| 612 | + | ||
| 613 | + console.log(`找到 ${modules.length} 个模块: ${modules.join(', ')}`) | ||
| 614 | + | ||
| 615 | + modules.forEach(moduleName => { | ||
| 616 | + const moduleDir = path.join(openAPIDir, moduleName) | ||
| 617 | + const apiFiles = fs | ||
| 618 | + .readdirSync(moduleDir) | ||
| 619 | + .filter(file => file.endsWith('.md') && file !== 'CLAUDE.md') | ||
| 620 | + | ||
| 621 | + if (apiFiles.length === 0) { | ||
| 622 | + console.log(`模块 ${moduleName} 中没有找到 .md 文件`) | ||
| 623 | + return | ||
| 624 | + } | ||
| 625 | + | ||
| 626 | + console.log(`\n处理模块: ${moduleName}`) | ||
| 627 | + console.log(`找到 ${apiFiles.length} 个 API 文档`) | ||
| 628 | + | ||
| 629 | + const apis = [] | ||
| 630 | + | ||
| 631 | + apiFiles.forEach(fileName => { | ||
| 632 | + const filePath = path.join(moduleDir, fileName) | ||
| 633 | + const content = fs.readFileSync(filePath, 'utf8') | ||
| 634 | + const yamlContent = extractYAMLFromMarkdown(content) | ||
| 635 | + | ||
| 636 | + if (!yamlContent) { | ||
| 637 | + console.warn(` ⚠️ ${fileName}: 未找到 YAML 代码块`) | ||
| 638 | + return | ||
| 639 | + } | ||
| 640 | + | ||
| 641 | + try { | ||
| 642 | + const openapiDoc = yaml.load(yamlContent) | ||
| 643 | + const apiName = path.basename(fileName, '.md') | ||
| 644 | + const apiInfo = parseOpenAPIDocument(openapiDoc, apiName) | ||
| 645 | + | ||
| 646 | + if (apiInfo) { | ||
| 647 | + apis.push(apiInfo) | ||
| 648 | + console.log(` ✓ ${apiName}: ${apiInfo.summary}`) | ||
| 649 | + } | ||
| 650 | + } catch (error) { | ||
| 651 | + console.error(` ✗ ${fileName}: 解析失败 - ${error.message}`) | ||
| 652 | + } | ||
| 653 | + }) | ||
| 654 | + | ||
| 655 | + // 生成并保存 API 文件 | ||
| 656 | + if (apis.length > 0) { | ||
| 657 | + const fileContent = generateApiFileContent(moduleName, apis) | ||
| 658 | + const outputPath = path.join(outputDir, `${moduleName}.js`) | ||
| 659 | + fs.writeFileSync(outputPath, fileContent, 'utf8') | ||
| 660 | + console.log(` 📝 生成文件: ${outputPath}`) | ||
| 661 | + } | ||
| 662 | + }) | ||
| 663 | + | ||
| 664 | + console.log('\n✅ API 文档生成完成!') | ||
| 665 | + | ||
| 666 | + // 对比新旧 API | ||
| 667 | + console.log('\n🔍 开始检测 API 变更...\n') | ||
| 668 | + compareAPIChanges(openAPIDir) | ||
| 669 | +} | ||
| 670 | + | ||
| 671 | +/** | ||
| 672 | + * 备份 OpenAPI 文档目录 | ||
| 673 | + * @param {string} sourceDir - 源目录 | ||
| 674 | + * @returns {string} - 备份目录路径 | ||
| 675 | + */ | ||
| 676 | +function backupOpenAPIDir(sourceDir) { | ||
| 677 | + const backupBaseDir = path.resolve(__dirname, '../.tmp') | ||
| 678 | + const backupDir = path.join(backupBaseDir, 'api-specs-backup') | ||
| 679 | + | ||
| 680 | + // 创建备份目录 | ||
| 681 | + if (!fs.existsSync(backupBaseDir)) { | ||
| 682 | + fs.mkdirSync(backupBaseDir, { recursive: true }) | ||
| 683 | + } | ||
| 684 | + | ||
| 685 | + // 删除旧备份 | ||
| 686 | + if (fs.existsSync(backupDir)) { | ||
| 687 | + fs.rmSync(backupDir, { recursive: true, force: true }) | ||
| 688 | + } | ||
| 689 | + | ||
| 690 | + // 复制目录 | ||
| 691 | + copyDirectory(sourceDir, backupDir) | ||
| 692 | + | ||
| 693 | + return backupDir | ||
| 694 | +} | ||
| 695 | + | ||
| 696 | +/** | ||
| 697 | + * 递归复制目录 | ||
| 698 | + * @param {string} src - 源路径 | ||
| 699 | + * @param {string} dest - 目标路径 | ||
| 700 | + */ | ||
| 701 | +function copyDirectory(src, dest) { | ||
| 702 | + if (!fs.existsSync(dest)) { | ||
| 703 | + fs.mkdirSync(dest, { recursive: true }) | ||
| 704 | + } | ||
| 705 | + | ||
| 706 | + const entries = fs.readdirSync(src, { withFileTypes: true }) | ||
| 707 | + | ||
| 708 | + for (const entry of entries) { | ||
| 709 | + const srcPath = path.join(src, entry.name) | ||
| 710 | + const destPath = path.join(dest, entry.name) | ||
| 711 | + | ||
| 712 | + if (entry.isDirectory()) { | ||
| 713 | + copyDirectory(srcPath, destPath) | ||
| 714 | + } else { | ||
| 715 | + fs.copyFileSync(srcPath, destPath) | ||
| 716 | + } | ||
| 717 | + } | ||
| 718 | +} | ||
| 719 | + | ||
| 720 | +/** | ||
| 721 | + * 对比新旧 API 变更 | ||
| 722 | + * @param {string} openAPIDir - OpenAPI 文档目录 | ||
| 723 | + */ | ||
| 724 | +function compareAPIChanges(openAPIDir) { | ||
| 725 | + const backupDir = path.resolve(__dirname, '../.tmp/api-specs-backup') | ||
| 726 | + const tempDir = path.resolve(__dirname, '../.tmp/api-specs-temp') | ||
| 727 | + | ||
| 728 | + // 检查是否存在临时备份(上一次的版本) | ||
| 729 | + if (!fs.existsSync(tempDir)) { | ||
| 730 | + console.log('ℹ️ 首次运行,已建立基线。下次运行将检测 API 变更。') | ||
| 731 | + // 将当前备份移动到临时目录,作为下次对比的基线 | ||
| 732 | + if (fs.existsSync(backupDir)) { | ||
| 733 | + fs.renameSync(backupDir, tempDir) | ||
| 734 | + } | ||
| 735 | + return | ||
| 736 | + } | ||
| 737 | + | ||
| 738 | + // 扫描模块 | ||
| 739 | + const modules = fs | ||
| 740 | + .readdirSync(openAPIDir, { withFileTypes: true }) | ||
| 741 | + .filter(dirent => dirent.isDirectory()) | ||
| 742 | + .map(dirent => dirent.name) | ||
| 743 | + | ||
| 744 | + let hasChanges = false | ||
| 745 | + const moduleReports = [] | ||
| 746 | + | ||
| 747 | + modules.forEach(moduleName => { | ||
| 748 | + const moduleDir = path.join(openAPIDir, moduleName) | ||
| 749 | + const tempModuleDir = path.join(tempDir, moduleName) | ||
| 750 | + | ||
| 751 | + // 如果临时备份中不存在该模块,说明是新增模块 | ||
| 752 | + if (!fs.existsSync(tempModuleDir)) { | ||
| 753 | + console.log(`\n📦 新增模块: ${moduleName}`) | ||
| 754 | + // 解析新增模块的接口 | ||
| 755 | + try { | ||
| 756 | + const newDocs = parseOpenAPIPath(moduleDir) | ||
| 757 | + if (newDocs && newDocs.length > 0) { | ||
| 758 | + // 显示新增接口信息 | ||
| 759 | + console.log(` 包含 ${newDocs.length} 个新增接口:`) | ||
| 760 | + newDocs.forEach(doc => { | ||
| 761 | + const path = Object.keys(doc.paths || {})[0] || '' | ||
| 762 | + const method = Object.keys(doc.paths?.[path] || {})[0] || '' | ||
| 763 | + const apiInfo = doc.paths?.[path]?.[method] | ||
| 764 | + const summary = apiInfo?.summary || doc.info?.title || '未命名接口' | ||
| 765 | + console.log(` • ${method?.toUpperCase()} ${path} - ${summary}`) | ||
| 766 | + }) | ||
| 767 | + } | ||
| 768 | + } catch (error) { | ||
| 769 | + console.error(` ⚠️ 解析模块失败: ${error.message}`) | ||
| 770 | + } | ||
| 771 | + hasChanges = true | ||
| 772 | + return | ||
| 773 | + } | ||
| 774 | + | ||
| 775 | + // 读取当前和临时备份的文档 | ||
| 776 | + const currentFiles = fs | ||
| 777 | + .readdirSync(moduleDir) | ||
| 778 | + .filter(f => f.endsWith('.md') && f !== 'CLAUDE.md') | ||
| 779 | + const tempFiles = fs | ||
| 780 | + .readdirSync(tempModuleDir) | ||
| 781 | + .filter(f => f.endsWith('.md') && f !== 'CLAUDE.md') | ||
| 782 | + | ||
| 783 | + // 检查是否有文件变更 | ||
| 784 | + const hasNewFiles = currentFiles.some(f => !tempFiles.includes(f)) | ||
| 785 | + const hasRemovedFiles = tempFiles.some(f => !currentFiles.includes(f)) | ||
| 786 | + const hasModifiedFiles = currentFiles.some(f => { | ||
| 787 | + if (!tempFiles.includes(f)) { | ||
| 788 | + return false | ||
| 789 | + } | ||
| 790 | + const currentContent = fs.readFileSync(path.join(moduleDir, f), 'utf8') | ||
| 791 | + const tempContent = fs.readFileSync(path.join(tempModuleDir, f), 'utf8') | ||
| 792 | + return currentContent !== tempContent | ||
| 793 | + }) | ||
| 794 | + | ||
| 795 | + if (hasNewFiles || hasRemovedFiles || hasModifiedFiles) { | ||
| 796 | + hasChanges = true | ||
| 797 | + moduleReports.push({ moduleName, moduleDir, tempModuleDir }) | ||
| 798 | + } | ||
| 799 | + }) | ||
| 800 | + | ||
| 801 | + // 检查删除的模块 | ||
| 802 | + const tempModules = fs.existsSync(tempDir) | ||
| 803 | + ? fs | ||
| 804 | + .readdirSync(tempDir, { withFileTypes: true }) | ||
| 805 | + .filter(dirent => dirent.isDirectory()) | ||
| 806 | + .map(dirent => dirent.name) | ||
| 807 | + : [] | ||
| 808 | + | ||
| 809 | + const deletedModules = tempModules.filter(m => !modules.includes(m)) | ||
| 810 | + if (deletedModules.length > 0) { | ||
| 811 | + hasChanges = true | ||
| 812 | + console.log(`\n❌ 删除模块: ${deletedModules.join(', ')}`) | ||
| 813 | + } | ||
| 814 | + | ||
| 815 | + if (!hasChanges) { | ||
| 816 | + console.log('✅ 未检测到 API 变更') | ||
| 817 | + // 更新基线 | ||
| 818 | + if (fs.existsSync(backupDir)) { | ||
| 819 | + fs.rmSync(tempDir, { recursive: true, force: true }) | ||
| 820 | + fs.renameSync(backupDir, tempDir) | ||
| 821 | + } | ||
| 822 | + return | ||
| 823 | + } | ||
| 824 | + | ||
| 825 | + // 逐个模块对比 | ||
| 826 | + console.log('') | ||
| 827 | + moduleReports.forEach(({ moduleName, moduleDir, tempModuleDir }) => { | ||
| 828 | + try { | ||
| 829 | + const oldDocs = parseOpenAPIPath(tempModuleDir) | ||
| 830 | + const newDocs = parseOpenAPIPath(moduleDir) | ||
| 831 | + const report = generateReport(oldDocs, newDocs, 'text') | ||
| 832 | + | ||
| 833 | + console.log(report) | ||
| 834 | + console.log('') | ||
| 835 | + } catch (error) { | ||
| 836 | + console.error(`⚠️ 模块 ${moduleName} 对比失败: ${error.message}`) | ||
| 837 | + } | ||
| 838 | + }) | ||
| 839 | + | ||
| 840 | + // 更新基线:将当前备份作为下次对比的基准 | ||
| 841 | + console.log('📝 更新 API 基线...') | ||
| 842 | + if (fs.existsSync(tempDir)) { | ||
| 843 | + fs.rmSync(tempDir, { recursive: true, force: true }) | ||
| 844 | + } | ||
| 845 | + if (fs.existsSync(backupDir)) { | ||
| 846 | + fs.renameSync(backupDir, tempDir) | ||
| 847 | + } | ||
| 848 | +} | ||
| 849 | + | ||
| 850 | +// 执行生成 | ||
| 851 | +const openAPIDir = path.resolve(__dirname, '../docs/api-specs') | ||
| 852 | +const outputDir = path.resolve(__dirname, '../src/api') | ||
| 853 | + | ||
| 854 | +console.log('=== OpenAPI 转 API 文档生成器 ===\n') | ||
| 855 | +console.log(`输入目录: ${openAPIDir}`) | ||
| 856 | +console.log(`输出目录: ${outputDir}\n`) | ||
| 857 | + | ||
| 858 | +// 备份当前的 OpenAPI 文档(用于下次对比) | ||
| 859 | +if (fs.existsSync(openAPIDir)) { | ||
| 860 | + console.log('💾 备份当前 OpenAPI 文档...') | ||
| 861 | + backupOpenAPIDir(openAPIDir) | ||
| 862 | + console.log('') | ||
| 863 | +} | ||
| 864 | + | ||
| 865 | +scanAndGenerate(openAPIDir, outputDir) |
scripts/test-generate.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 测试生成的 API 文件 | ||
| 3 | + */ | ||
| 4 | + | ||
| 5 | +const path = require('path') | ||
| 6 | +const fs = require('fs') | ||
| 7 | + | ||
| 8 | +// 测试导入生成的 API | ||
| 9 | +const userApiPath = path.resolve(__dirname, '../src/api/user.js') | ||
| 10 | + | ||
| 11 | +console.log('=== 测试生成的 API 文件 ===\n') | ||
| 12 | + | ||
| 13 | +if (fs.existsSync(userApiPath)) { | ||
| 14 | + const content = fs.readFileSync(userApiPath, 'utf8') | ||
| 15 | + console.log('✅ API 文件生成成功\n') | ||
| 16 | + console.log('文件内容:') | ||
| 17 | + console.log('─'.repeat(60)) | ||
| 18 | + console.log(content) | ||
| 19 | + console.log('─'.repeat(60)) | ||
| 20 | + | ||
| 21 | + // 验证关键部分 | ||
| 22 | + const checks = [ | ||
| 23 | + { name: '导入 fn 和 fetch', pattern: /import \{ fn, fetch \} from '@\/api\/fn'/ }, | ||
| 24 | + { name: 'Api 常量定义', pattern: /const Api = \{/ }, | ||
| 25 | + { name: '导出函数', pattern: /export const getUserInfoAPI/ }, | ||
| 26 | + { name: 'JSDoc 注释', pattern: /\/\*\*[\s\S]*?\*\// }, | ||
| 27 | + { name: '正确的 action', pattern: /a=user_info/ }, | ||
| 28 | + ] | ||
| 29 | + | ||
| 30 | + console.log('\n验证结果:') | ||
| 31 | + checks.forEach(check => { | ||
| 32 | + const passed = check.pattern.test(content) | ||
| 33 | + console.log(`${passed ? '✅' : '❌'} ${check.name}`) | ||
| 34 | + }) | ||
| 35 | + | ||
| 36 | + console.log('\n✅ 所有验证通过!') | ||
| 37 | +} else { | ||
| 38 | + console.log('❌ API 文件不存在,请先运行 pnpm api:generate') | ||
| 39 | +} |
src/api/map_activity.js
0 → 100644
| 1 | +import { fn, fetch } from '@/api/fn' | ||
| 2 | + | ||
| 3 | +const Api = { | ||
| 4 | + Checkin: '/srv/?a=map_activity&t=checkin', | ||
| 5 | + Detail: '/srv/?a=map_activity&t=detail', | ||
| 6 | + IsChecked: '/srv/?a=map_activity&t=is_checked', | ||
| 7 | + List: '/srv/?a=map_activity&t=list', | ||
| 8 | + Poster: '/srv/?a=map_activity&t=poster', | ||
| 9 | + SavePosterBackground: '/srv/?a=map_activity&t=save_poster_background', | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * @description 打卡 | ||
| 14 | + * @remark | ||
| 15 | + * @param {Object} params 请求参数 | ||
| 16 | + * @param {string} params.activity_id (可选) 活动ID | ||
| 17 | + * @param {string} params.detail_id (可选) 打卡点ID | ||
| 18 | + * @param {string} params.openid (可选) | ||
| 19 | + * @returns {Promise<{ | ||
| 20 | + * code: number; // 状态码 | ||
| 21 | + * msg: string; // 消息 | ||
| 22 | + * data: any; | ||
| 23 | + * }>} | ||
| 24 | + */ | ||
| 25 | +export const checkinAPI = params => fn(fetch.post(Api.Checkin, params)) | ||
| 26 | + | ||
| 27 | +/** | ||
| 28 | + * @description 地图活动详情 | ||
| 29 | + * @remark | ||
| 30 | + * @param {Object} params 请求参数 | ||
| 31 | + * @param {string} params.id (可选) 活动ID | ||
| 32 | + * @returns {Promise<{ | ||
| 33 | + * code: number; // 状态码 | ||
| 34 | + * msg: string; // 消息 | ||
| 35 | + * data: { | ||
| 36 | + url: string; // 地图网址 | ||
| 37 | + id: string; // 活动ID | ||
| 38 | + cover: string; // 封面图 | ||
| 39 | + tittle: string; // 标题 | ||
| 40 | + begin_date: string; // 开始时间 | ||
| 41 | + end_date: string; // 结束时间 | ||
| 42 | + is_ended: boolean; // 活动是否已经结束 | ||
| 43 | + is_begin: boolean; // 活动是否开始 | ||
| 44 | + first_checkin_points: integer; // 首次打卡获得积分 | ||
| 45 | + required_checkin_count: integer; // 需要打卡几次,才能完成活动 | ||
| 46 | + complete_points: integer; // 完成活动获得多少积分 | ||
| 47 | + discount_title: string; // 打卡点底部优惠标题 | ||
| 48 | + * }; | ||
| 49 | + * }>} | ||
| 50 | + */ | ||
| 51 | +export const detailAPI = params => fn(fetch.get(Api.Detail, params)) | ||
| 52 | + | ||
| 53 | +/** | ||
| 54 | + * @description 是否已经打卡 | ||
| 55 | + * @remark | ||
| 56 | + * @param {Object} params 请求参数 | ||
| 57 | + * @param {string} params.detail_id (可选) 打卡点ID | ||
| 58 | + * @param {string} params.openid (可选) | ||
| 59 | + * @param {string} params.activity_id (可选) 活动ID | ||
| 60 | + * @returns {Promise<{ | ||
| 61 | + * code: number; // 状态码 | ||
| 62 | + * msg: string; // 消息 | ||
| 63 | + * data: { | ||
| 64 | + is_checked: boolean; // 是否已经打卡 | ||
| 65 | + * }; | ||
| 66 | + * }>} | ||
| 67 | + */ | ||
| 68 | +export const isCheckedAPI = params => fn(fetch.get(Api.IsChecked, params)) | ||
| 69 | + | ||
| 70 | +/** | ||
| 71 | + * @description 地图活动列表 | ||
| 72 | + * @remark | ||
| 73 | + * @param {Object} params 请求参数 | ||
| 74 | + * @returns {Promise<{ | ||
| 75 | + * code: number; // 状态码 | ||
| 76 | + * msg: string; // 消息 | ||
| 77 | + * data: Array<{ | ||
| 78 | + url: string; // 地图网址 | ||
| 79 | + id: string; // 活动ID | ||
| 80 | + cover: string; // 封面图 | ||
| 81 | + tittle: string; // 标题 | ||
| 82 | + begin_date: string; // 开始时间 | ||
| 83 | + end_date: string; // 结束时间 | ||
| 84 | + * }>; | ||
| 85 | + * }>} | ||
| 86 | + */ | ||
| 87 | +export const listAPI = params => fn(fetch.get(Api.List, params)) | ||
| 88 | + | ||
| 89 | +/** | ||
| 90 | + * @description 获取海报 | ||
| 91 | + * @remark | ||
| 92 | + * @param {Object} params 请求参数 | ||
| 93 | + * @param {string} params.activity_id (可选) 活动ID | ||
| 94 | + * @param {string} params.detail_id (可选) 关卡ID | ||
| 95 | + * @param {string} params.env_version (可选) 小程序版本。正式版为 "release",体验版为 "trial"。默认是正式版 | ||
| 96 | + * @returns {Promise<{ | ||
| 97 | + * code: number; // 状态码 | ||
| 98 | + * msg: string; // 消息 | ||
| 99 | + * data: { | ||
| 100 | + details: Array<{ | ||
| 101 | + id: integer; // 关卡ID | ||
| 102 | + name: string; // 关卡名称 | ||
| 103 | + background_url: string; // 关卡背景图 | ||
| 104 | + main_slogan: string; // | ||
| 105 | + sub_slogan: string; // | ||
| 106 | + is_checked: boolean; // 是否已经打卡 | ||
| 107 | + }>; | ||
| 108 | + family: { | ||
| 109 | + id: integer; // 家庭ID | ||
| 110 | + name: string; // 家庭名称 | ||
| 111 | + avatar_url: string; // 家庭头像 | ||
| 112 | + }; | ||
| 113 | + show_detail_index: integer; // 从 0 开始计数 | ||
| 114 | + end_date: string; // 活动截止时间 | ||
| 115 | + qrcode_url: string; // 小程序码 | ||
| 116 | + title: string; // 海报标题 | ||
| 117 | + begin_date: string; // 活动开始日期 | ||
| 118 | + * }; | ||
| 119 | + * }>} | ||
| 120 | + */ | ||
| 121 | +export const posterAPI = params => fn(fetch.get(Api.Poster, params)) | ||
| 122 | + | ||
| 123 | +/** | ||
| 124 | + * @description 上传海报背景 | ||
| 125 | + * @remark | ||
| 126 | + * @param {Object} params 请求参数 | ||
| 127 | + * @param {string} params.activity_id (可选) 活动ID | ||
| 128 | + * @param {string} params.detail_id (可选) 打卡点ID | ||
| 129 | + * @param {string} params.poster_background_url (可选) 关卡海报背景 | ||
| 130 | + * @returns {Promise<{ | ||
| 131 | + * code: number; // 状态码 | ||
| 132 | + * msg: string; // 消息 | ||
| 133 | + * data: any; | ||
| 134 | + * }>} | ||
| 135 | + */ | ||
| 136 | +export const savePosterBackgroundAPI = params => fn(fetch.post(Api.SavePosterBackground, params)) |
| ... | @@ -37,54 +37,68 @@ | ... | @@ -37,54 +37,68 @@ |
| 37 | import { ref } from 'vue' | 37 | import { ref } from 'vue' |
| 38 | import Taro from '@tarojs/taro' | 38 | import Taro from '@tarojs/taro' |
| 39 | import BottomNav from '@/components/BottomNav.vue' | 39 | import BottomNav from '@/components/BottomNav.vue' |
| 40 | +import { listAPI } from '@/api/map_activity' | ||
| 41 | +import { mockMapActivityListAPI } from '@/utils/mockData' | ||
| 42 | +import { useLoad } from '@tarojs/taro' | ||
| 43 | + | ||
| 44 | +// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API | ||
| 45 | +const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | ||
| 46 | + | ||
| 47 | +/** | ||
| 48 | + * 便民地图列表数据 | ||
| 49 | + */ | ||
| 50 | +const mapList = ref([]) | ||
| 51 | +const loading = ref(false) | ||
| 52 | + | ||
| 53 | +/** | ||
| 54 | + * 格式化 API 数据为页面所需格式 | ||
| 55 | + * @param {Array} list - API 返回的活动列表 | ||
| 56 | + * @returns {Array} 格式化后的活动列表 | ||
| 57 | + */ | ||
| 58 | +const formatMapList = list => { | ||
| 59 | + return list.map(item => ({ | ||
| 60 | + id: item.id, | ||
| 61 | + title: item.tittle, // API 返回的是 tittle,映射为 title | ||
| 62 | + cover: item.cover, | ||
| 63 | + timeRange: `${item.begin_date}~${item.end_date}`, | ||
| 64 | + activityId: item.id, // 使用 id 作为 activityId | ||
| 65 | + })) | ||
| 66 | +} | ||
| 40 | 67 | ||
| 41 | /** | 68 | /** |
| 42 | - * Mock 便民地图数据 | 69 | + * 获取地图活动列表 |
| 43 | */ | 70 | */ |
| 44 | -const mapList = ref([ | 71 | +const fetchMapList = async () => { |
| 45 | - { | 72 | + if (loading.value) { |
| 46 | - id: 1, | 73 | + return |
| 47 | - title: '重阳登高打卡', | 74 | + } |
| 48 | - cover: 'https://picsum.photos/400/300?random=1', | 75 | + |
| 49 | - timeRange: '2025.09.06~2025.10.31', | 76 | + loading.value = true |
| 50 | - activityId: 'chongyang_2024', | 77 | + |
| 51 | - }, | 78 | + try { |
| 52 | - { | 79 | + const params = {} |
| 53 | - id: 2, | 80 | + |
| 54 | - title: '公园晨跑打卡', | 81 | + // 根据开关选择使用真实 API 或 Mock 数据 |
| 55 | - cover: 'https://picsum.photos/400/300?random=2', | 82 | + const res = USE_MOCK_DATA ? await mockMapActivityListAPI(params) : await listAPI(params) |
| 56 | - timeRange: '2025.09.01~2025.12.31', | 83 | + |
| 57 | - activityId: 'morning_run_2024', | 84 | + if (res.code === 1 && res.data) { |
| 58 | - }, | 85 | + mapList.value = formatMapList(res.data) |
| 59 | - { | 86 | + } else { |
| 60 | - id: 3, | 87 | + Taro.showToast({ |
| 61 | - title: '社区健身打卡', | 88 | + title: res.msg || '获取活动列表失败', |
| 62 | - cover: 'https://picsum.photos/400/300?random=3', | 89 | + icon: 'none', |
| 63 | - timeRange: '2025.08.01~2025.12.31', | 90 | + }) |
| 64 | - activityId: 'community_fitness', | 91 | + } |
| 65 | - }, | 92 | + } catch (err) { |
| 66 | - { | 93 | + console.error('获取地图活动列表失败:', err) |
| 67 | - id: 4, | 94 | + Taro.showToast({ |
| 68 | - title: '周末徒步打卡', | 95 | + title: '网络异常,请重试', |
| 69 | - cover: 'https://picsum.photos/400/300?random=4', | 96 | + icon: 'none', |
| 70 | - timeRange: '2025.09.15~2025.11.30', | 97 | + }) |
| 71 | - activityId: 'weekend_hike', | 98 | + } finally { |
| 72 | - }, | 99 | + loading.value = false |
| 73 | - { | 100 | + } |
| 74 | - id: 5, | 101 | +} |
| 75 | - title: '秋日赏菊打卡', | ||
| 76 | - cover: 'https://picsum.photos/400/300?random=5', | ||
| 77 | - timeRange: '2025.10.15~2025.11.15', | ||
| 78 | - activityId: 'autumn_chrysanthemum', | ||
| 79 | - }, | ||
| 80 | - { | ||
| 81 | - id: 6, | ||
| 82 | - title: '古镇文化打卡', | ||
| 83 | - cover: 'https://picsum.photos/400/300?random=6', | ||
| 84 | - timeRange: '2025.10.01~2025.10.31', | ||
| 85 | - activityId: 'ancient_town', | ||
| 86 | - }, | ||
| 87 | -]) | ||
| 88 | 102 | ||
| 89 | /** | 103 | /** |
| 90 | * 处理卡片点击事件 | 104 | * 处理卡片点击事件 |
| ... | @@ -107,6 +121,11 @@ const handleEnter = item => { | ... | @@ -107,6 +121,11 @@ const handleEnter = item => { |
| 107 | url: `/pages/ActivitiesCover/index?activityId=${item.activityId}&title=${encodeURIComponent(item.title)}`, | 121 | url: `/pages/ActivitiesCover/index?activityId=${item.activityId}&title=${encodeURIComponent(item.title)}`, |
| 108 | }) | 122 | }) |
| 109 | } | 123 | } |
| 124 | + | ||
| 125 | +// 页面加载时获取列表 | ||
| 126 | +useLoad(() => { | ||
| 127 | + fetchMapList() | ||
| 128 | +}) | ||
| 110 | </script> | 129 | </script> |
| 111 | 130 | ||
| 112 | <style lang="less"> | 131 | <style lang="less"> | ... | ... |
src/utils/mockData.js
0 → 100644
| 1 | +/** | ||
| 2 | + * @Description: Mock 数据生成工具 - 用于测试地图活动功能 | ||
| 3 | + * @Date: 2026-02-09 | ||
| 4 | + * | ||
| 5 | + * 支持的 API Mock: | ||
| 6 | + * - listAPI: 地图活动列表 | ||
| 7 | + * - detailAPI: 地图活动详情 | ||
| 8 | + * - isCheckedAPI: 是否已打卡 | ||
| 9 | + * - posterAPI: 获取海报 | ||
| 10 | + */ | ||
| 11 | + | ||
| 12 | +// ============================================================================ | ||
| 13 | +// 工具函数 | ||
| 14 | +// ============================================================================ | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * 随机图片 URL 生成器 | ||
| 18 | + * @param {number} width - 图片宽度 | ||
| 19 | + * @param {number} height - 图片高度 | ||
| 20 | + * @param {number} seed - 随机种子 | ||
| 21 | + * @returns {string} 图片 URL | ||
| 22 | + */ | ||
| 23 | +function randomImage(width = 400, height = 300, seed = 1) { | ||
| 24 | + return `https://picsum.photos/${width}/${height}?random=${seed}` | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +/** | ||
| 28 | + * 模拟网络延迟 | ||
| 29 | + * @param {number} min - 最小延迟(ms) | ||
| 30 | + * @param {number} max - 最大延迟(ms) | ||
| 31 | + * @returns {Promise} | ||
| 32 | + */ | ||
| 33 | +function mockDelay(min = 100, max = 300) { | ||
| 34 | + const delay = Math.random() * (max - min) + min | ||
| 35 | + return new Promise(resolve => setTimeout(resolve, delay)) | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +// ============================================================================ | ||
| 39 | +// 1. 地图活动列表 Mock (listAPI) | ||
| 40 | +// ============================================================================ | ||
| 41 | + | ||
| 42 | +const ACTIVITY_NAMES = [ | ||
| 43 | + '重阳登高打卡', | ||
| 44 | + '公园晨跑打卡', | ||
| 45 | + '社区健身打卡', | ||
| 46 | + '周末徒步打卡', | ||
| 47 | + '秋日赏菊打卡', | ||
| 48 | + '古镇文化打卡', | ||
| 49 | + '健康骑行活动', | ||
| 50 | + '亲子运动会', | ||
| 51 | + '户外拓展训练', | ||
| 52 | + '城市定向挑战', | ||
| 53 | +] | ||
| 54 | + | ||
| 55 | +/** | ||
| 56 | + * 生成地图活动列表项 | ||
| 57 | + * @param {number} id - 活动 ID | ||
| 58 | + * @returns {Object} 活动对象 | ||
| 59 | + */ | ||
| 60 | +function generateMapActivityItem(id) { | ||
| 61 | + const activityName = ACTIVITY_NAMES[Math.floor(Math.random() * ACTIVITY_NAMES.length)] | ||
| 62 | + const now = new Date() | ||
| 63 | + const startDate = new Date(now.getTime() + Math.random() * 30 * 24 * 60 * 60 * 1000) | ||
| 64 | + const endDate = new Date(startDate.getTime() + (30 + Math.random() * 60) * 24 * 60 * 60 * 1000) | ||
| 65 | + | ||
| 66 | + // 格式化日期为 YYYY.MM.DD | ||
| 67 | + const formatDate = date => { | ||
| 68 | + const year = date.getFullYear() | ||
| 69 | + const month = String(date.getMonth() + 1).padStart(2, '0') | ||
| 70 | + const day = String(date.getDate()).padStart(2, '0') | ||
| 71 | + return `${year}.${month}.${day}` | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + return { | ||
| 75 | + id: String(id), | ||
| 76 | + tittle: activityName, | ||
| 77 | + cover: randomImage(400, 300, id), | ||
| 78 | + begin_date: formatDate(startDate), | ||
| 79 | + end_date: formatDate(endDate), | ||
| 80 | + url: '', | ||
| 81 | + } | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +/** | ||
| 85 | + * Mock: listAPI (地图活动列表) | ||
| 86 | + * @param {Object} params - 请求参数 | ||
| 87 | + * @returns {Promise<{code: number, msg: string, data: Array}>} | ||
| 88 | + */ | ||
| 89 | +export async function mockMapActivityListAPI() { | ||
| 90 | + await mockDelay() | ||
| 91 | + | ||
| 92 | + const list = [] | ||
| 93 | + const total = 6 | ||
| 94 | + | ||
| 95 | + for (let i = 0; i < total; i++) { | ||
| 96 | + list.push(generateMapActivityItem(i + 1)) | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + console.log(`[Mock] listAPI - 地图活动列表,共${list.length}条`) | ||
| 100 | + | ||
| 101 | + return { | ||
| 102 | + code: 1, | ||
| 103 | + msg: 'success', | ||
| 104 | + data: list, | ||
| 105 | + } | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +// ============================================================================ | ||
| 109 | +// 2. 地图活动详情 Mock (detailAPI) | ||
| 110 | +// ============================================================================ | ||
| 111 | + | ||
| 112 | +/** | ||
| 113 | + * Mock: detailAPI (地图活动详情) | ||
| 114 | + * @param {Object} params - 请求参数 | ||
| 115 | + * @param {string} params.id - 活动 ID | ||
| 116 | + * @returns {Promise<{code: number, msg: string, data: Object}>} | ||
| 117 | + */ | ||
| 118 | +export async function mockMapActivityDetailAPI(params) { | ||
| 119 | + await mockDelay() | ||
| 120 | + | ||
| 121 | + const { id } = params | ||
| 122 | + const item = generateMapActivityItem(parseInt(id) || 1) | ||
| 123 | + | ||
| 124 | + console.log(`[Mock] detailAPI - 活动详情,ID:${id}`) | ||
| 125 | + | ||
| 126 | + return { | ||
| 127 | + code: 1, | ||
| 128 | + msg: 'success', | ||
| 129 | + data: { | ||
| 130 | + ...item, | ||
| 131 | + url: 'https://example.com/map', | ||
| 132 | + is_ended: false, | ||
| 133 | + is_begin: true, | ||
| 134 | + first_checkin_points: 10, | ||
| 135 | + required_checkin_count: 5, | ||
| 136 | + complete_points: 50, | ||
| 137 | + discount_title: '打卡点优惠信息', | ||
| 138 | + }, | ||
| 139 | + } | ||
| 140 | +} | ||
| 141 | + | ||
| 142 | +// ============================================================================ | ||
| 143 | +// 3. 是否已打卡 Mock (isCheckedAPI) | ||
| 144 | +// ============================================================================ | ||
| 145 | + | ||
| 146 | +/** | ||
| 147 | + * Mock: isCheckedAPI (是否已打卡) | ||
| 148 | + * @param {Object} params - 请求参数 | ||
| 149 | + * @param {string} params.detail_id - 打卡点 ID | ||
| 150 | + * @returns {Promise<{code: number, msg: string, data: Object}>} | ||
| 151 | + */ | ||
| 152 | +export async function mockIsCheckedAPI(params) { | ||
| 153 | + await mockDelay() | ||
| 154 | + | ||
| 155 | + const { detail_id } = params | ||
| 156 | + const isChecked = parseInt(detail_id) % 3 === 0 | ||
| 157 | + | ||
| 158 | + console.log(`[Mock] isCheckedAPI - 打卡点${detail_id},${isChecked ? '已打卡' : '未打卡'}`) | ||
| 159 | + | ||
| 160 | + return { | ||
| 161 | + code: 1, | ||
| 162 | + msg: 'success', | ||
| 163 | + data: { | ||
| 164 | + is_checked: isChecked, | ||
| 165 | + }, | ||
| 166 | + } | ||
| 167 | +} | ||
| 168 | + | ||
| 169 | +// ============================================================================ | ||
| 170 | +// 4. 获取海报 Mock (posterAPI) | ||
| 171 | +// ============================================================================ | ||
| 172 | + | ||
| 173 | +/** | ||
| 174 | + * Mock: posterAPI (获取海报) | ||
| 175 | + * @param {Object} params - 请求参数 | ||
| 176 | + * @param {string} params.activity_id - 活动 ID | ||
| 177 | + * @returns {Promise<{code: number, msg: string, data: Object}>} | ||
| 178 | + */ | ||
| 179 | +export async function mockPosterAPI(params) { | ||
| 180 | + await mockDelay() | ||
| 181 | + | ||
| 182 | + const { activity_id } = params | ||
| 183 | + | ||
| 184 | + console.log(`[Mock] posterAPI - 获取海报,活动ID:${activity_id}`) | ||
| 185 | + | ||
| 186 | + return { | ||
| 187 | + code: 1, | ||
| 188 | + msg: 'success', | ||
| 189 | + data: { | ||
| 190 | + details: [ | ||
| 191 | + { | ||
| 192 | + id: 1, | ||
| 193 | + name: '起点打卡', | ||
| 194 | + background_url: randomImage(750, 1200, 20), | ||
| 195 | + main_slogan: '开启健康之旅', | ||
| 196 | + sub_slogan: '坚持就是胜利', | ||
| 197 | + is_checked: true, | ||
| 198 | + }, | ||
| 199 | + { | ||
| 200 | + id: 2, | ||
| 201 | + name: '山顶打卡', | ||
| 202 | + background_url: randomImage(750, 1200, 21), | ||
| 203 | + main_slogan: '登高望远', | ||
| 204 | + sub_slogan: '风景这边独好', | ||
| 205 | + is_checked: false, | ||
| 206 | + }, | ||
| 207 | + ], | ||
| 208 | + family: { | ||
| 209 | + id: 123, | ||
| 210 | + name: '快乐家庭', | ||
| 211 | + avatar_url: randomImage(200, 200, 30), | ||
| 212 | + }, | ||
| 213 | + show_detail_index: 0, | ||
| 214 | + end_date: '2025.10.31', | ||
| 215 | + qrcode_url: 'https://example.com/qrcode.jpg', | ||
| 216 | + title: '重阳登高打卡', | ||
| 217 | + begin_date: '2025.09.06', | ||
| 218 | + }, | ||
| 219 | + } | ||
| 220 | +} | ||
| 221 | + | ||
| 222 | +// ============================================================================ | ||
| 223 | +// 导出统一 Mock API 调用器 | ||
| 224 | +// ============================================================================ | ||
| 225 | + | ||
| 226 | +/** | ||
| 227 | + * Mock API 调用器 | ||
| 228 | + * @param {string} apiName - API 名称 | ||
| 229 | + * @param {Object} params - 请求参数 | ||
| 230 | + * @returns {Promise} | ||
| 231 | + */ | ||
| 232 | +export async function mockAPI(apiName, params) { | ||
| 233 | + switch (apiName) { | ||
| 234 | + case 'listAPI': | ||
| 235 | + return await mockMapActivityListAPI(params) | ||
| 236 | + case 'detailAPI': | ||
| 237 | + return await mockMapActivityDetailAPI(params) | ||
| 238 | + case 'isCheckedAPI': | ||
| 239 | + return await mockIsCheckedAPI(params) | ||
| 240 | + case 'posterAPI': | ||
| 241 | + return await mockPosterAPI(params) | ||
| 242 | + default: | ||
| 243 | + console.warn(`[Mock] 未知的 API: ${apiName}`) | ||
| 244 | + return { code: 0, msg: 'Unknown API', data: null } | ||
| 245 | + } | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +/** | ||
| 249 | + * Mock 地图活动详情数据 | ||
| 250 | + * @param {string} activityId - 活动 ID | ||
| 251 | + * @returns {Object} 地图活动详情 | ||
| 252 | + */ | ||
| 253 | +export const mockMapActivityDetail = () => { | ||
| 254 | + return { | ||
| 255 | + url: 'https://example.com/map', | ||
| 256 | + id: '1', | ||
| 257 | + cover: randomImage(750, 500, 10), | ||
| 258 | + tittle: '重阳登高打卡', | ||
| 259 | + begin_date: '2025.09.06', | ||
| 260 | + end_date: '2025.10.31', | ||
| 261 | + is_ended: false, | ||
| 262 | + is_begin: true, | ||
| 263 | + first_checkin_points: 10, | ||
| 264 | + required_checkin_count: 5, | ||
| 265 | + complete_points: 50, | ||
| 266 | + discount_title: '打卡点优惠信息', | ||
| 267 | + } | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +/** | ||
| 271 | + * Mock 是否已打卡数据 | ||
| 272 | + * @param {string} detailId - 打卡点 ID | ||
| 273 | + * @returns {boolean} 是否已打卡 | ||
| 274 | + */ | ||
| 275 | +export const mockIsChecked = detailId => { | ||
| 276 | + // 偶尔返回已打卡 | ||
| 277 | + return parseInt(detailId) % 3 === 0 | ||
| 278 | +} | ||
| 279 | + | ||
| 280 | +/** | ||
| 281 | + * Mock 海报数据 | ||
| 282 | + * @param {string} activityId - 活动 ID | ||
| 283 | + * @returns {Object} 海报数据 | ||
| 284 | + */ | ||
| 285 | +export const mockPoster = activityId => { | ||
| 286 | + return { | ||
| 287 | + details: [ | ||
| 288 | + { | ||
| 289 | + id: 1, | ||
| 290 | + name: '起点打卡', | ||
| 291 | + background_url: randomImage(750, 1200, 20), | ||
| 292 | + main_slogan: '开启健康之旅', | ||
| 293 | + sub_slogan: '坚持就是胜利', | ||
| 294 | + is_checked: true, | ||
| 295 | + }, | ||
| 296 | + { | ||
| 297 | + id: 2, | ||
| 298 | + name: '山顶打卡', | ||
| 299 | + background_url: randomImage(750, 1200, 21), | ||
| 300 | + main_slogan: '登高望远', | ||
| 301 | + sub_slogan: '风景这边独好', | ||
| 302 | + is_checked: false, | ||
| 303 | + }, | ||
| 304 | + ], | ||
| 305 | + family: { | ||
| 306 | + id: 123, | ||
| 307 | + name: '快乐家庭', | ||
| 308 | + avatar_url: randomImage(200, 200, 30), | ||
| 309 | + }, | ||
| 310 | + show_detail_index: 0, | ||
| 311 | + end_date: '2025.10.31', | ||
| 312 | + qrcode_url: 'https://example.com/qrcode.jpg', | ||
| 313 | + title: '重阳登高打卡', | ||
| 314 | + begin_date: '2025.09.06', | ||
| 315 | + } | ||
| 316 | +} |
-
Please register or login to post a comment