feat: 添加微信认证相关接口,包括微信登录和用户资料更新功能
- 新增 wechatLogin 接口,支持使用微信临时凭证 code 登录或注册用户,返回用户信息和 auth token。 - 新增 wechatProfile 接口,支持更新微信用户资料,支持非空字段增量更新。 - 定义相关请求和响应的 schema,包括 WechatLoginRequest、WechatProfileRequest、AuthSuccessResponse 和 WechatProfileResponse。 - 处理不同的响应状态码,包括成功、参数错误、认证失败等。
This commit is contained in:
268
.tmp-openapi-validate/add-openapi-examples.js
Normal file
268
.tmp-openapi-validate/add-openapi-examples.js
Normal file
@@ -0,0 +1,268 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const YAML = require('yaml');
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const rootFile = path.join(repoRoot, 'pocket-base', 'spec', 'openapi-wx.yaml');
|
||||
const splitDir = path.join(repoRoot, 'pocket-base', 'spec', 'openapi-wx');
|
||||
|
||||
const filePaths = [
|
||||
rootFile,
|
||||
...fs
|
||||
.readdirSync(splitDir)
|
||||
.filter((name) => name.endsWith('.yaml'))
|
||||
.map((name) => path.join(splitDir, name)),
|
||||
];
|
||||
|
||||
const docs = new Map(
|
||||
filePaths.map((filePath) => [filePath, YAML.parse(fs.readFileSync(filePath, 'utf8'))]),
|
||||
);
|
||||
|
||||
function decodePointerSegment(segment) {
|
||||
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
||||
}
|
||||
|
||||
function getByPointer(documentData, pointer) {
|
||||
if (!pointer || pointer === '#' || pointer === '') {
|
||||
return documentData;
|
||||
}
|
||||
|
||||
const segments = pointer
|
||||
.replace(/^#/, '')
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.map(decodePointerSegment);
|
||||
|
||||
let current = documentData;
|
||||
for (const segment of segments) {
|
||||
if (current == null) {
|
||||
return undefined;
|
||||
}
|
||||
current = current[segment];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function resolveRef(ref, currentFile) {
|
||||
const [filePart, hashPart = ''] = ref.split('#');
|
||||
const targetFile = filePart
|
||||
? path.resolve(path.dirname(currentFile), filePart)
|
||||
: currentFile;
|
||||
const targetDoc = docs.get(targetFile);
|
||||
if (!targetDoc) {
|
||||
return { targetFile, targetKey: `${targetFile}#${hashPart}`, schema: undefined };
|
||||
}
|
||||
|
||||
const pointer = hashPart ? `#${hashPart}` : '#';
|
||||
return {
|
||||
targetFile,
|
||||
targetKey: `${targetFile}${pointer}`,
|
||||
schema: getByPointer(targetDoc, pointer),
|
||||
};
|
||||
}
|
||||
|
||||
function pickType(typeValue) {
|
||||
if (Array.isArray(typeValue)) {
|
||||
return typeValue.find((item) => item !== 'null') || typeValue[0];
|
||||
}
|
||||
return typeValue;
|
||||
}
|
||||
|
||||
function cleanLabelText(text, fallback) {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const normalized = text
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => line.replace(/^[\-\d\.\)\s]+/, '').trim())
|
||||
.find((line) => line && !/^(可选|必填|说明|注意)[。::]?$/u.test(line));
|
||||
|
||||
if (!normalized) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return normalized.replace(/[`"'<>]/g, '').trim() || fallback;
|
||||
}
|
||||
|
||||
function placeholderKey(key) {
|
||||
return typeof key === 'string' && key.includes('|');
|
||||
}
|
||||
|
||||
function deepMerge(baseValue, nextValue) {
|
||||
if (
|
||||
baseValue &&
|
||||
nextValue &&
|
||||
typeof baseValue === 'object' &&
|
||||
typeof nextValue === 'object' &&
|
||||
!Array.isArray(baseValue) &&
|
||||
!Array.isArray(nextValue)
|
||||
) {
|
||||
const merged = { ...baseValue };
|
||||
const nextKeys = Object.keys(nextValue);
|
||||
const hasConcreteNextKeys = nextKeys.some((key) => !placeholderKey(key));
|
||||
|
||||
if (hasConcreteNextKeys) {
|
||||
for (const key of Object.keys(merged)) {
|
||||
if (placeholderKey(key)) {
|
||||
delete merged[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(nextValue)) {
|
||||
if (key in merged) {
|
||||
merged[key] = deepMerge(merged[key], value);
|
||||
} else {
|
||||
merged[key] = value;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
return nextValue;
|
||||
}
|
||||
|
||||
function buildLabel(schemaLike, fallback) {
|
||||
return cleanLabelText(schemaLike?.description || schemaLike?.title, fallback);
|
||||
}
|
||||
|
||||
function fallbackScalar(label, fieldType) {
|
||||
return `${label}|${fieldType || 'string'}`;
|
||||
}
|
||||
|
||||
function buildExample(schemaLike, currentFile, fieldName = 'field', seenRefs = new Set()) {
|
||||
if (schemaLike == null) {
|
||||
return fallbackScalar(fieldName, 'string');
|
||||
}
|
||||
|
||||
if (typeof schemaLike !== 'object' || Array.isArray(schemaLike)) {
|
||||
return fallbackScalar(fieldName, 'string');
|
||||
}
|
||||
|
||||
if (schemaLike.$ref) {
|
||||
const { targetFile, targetKey, schema } = resolveRef(schemaLike.$ref, currentFile);
|
||||
if (!schema) {
|
||||
return fallbackScalar(fieldName, 'string');
|
||||
}
|
||||
if (seenRefs.has(targetKey)) {
|
||||
return fallbackScalar(fieldName, 'object');
|
||||
}
|
||||
const nextSeen = new Set(seenRefs);
|
||||
nextSeen.add(targetKey);
|
||||
return buildExample(schema, targetFile, fieldName, nextSeen);
|
||||
}
|
||||
|
||||
if (schemaLike.allOf) {
|
||||
return schemaLike.allOf.reduce((accumulator, item) => {
|
||||
const nextValue = buildExample(item, currentFile, fieldName, seenRefs);
|
||||
return deepMerge(accumulator, nextValue);
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (schemaLike.oneOf && schemaLike.oneOf.length > 0) {
|
||||
return buildExample(schemaLike.oneOf[0], currentFile, fieldName, seenRefs);
|
||||
}
|
||||
|
||||
if (schemaLike.anyOf && schemaLike.anyOf.length > 0) {
|
||||
return buildExample(schemaLike.anyOf[0], currentFile, fieldName, seenRefs);
|
||||
}
|
||||
|
||||
const fieldType =
|
||||
pickType(schemaLike.type) ||
|
||||
(schemaLike.properties || schemaLike.additionalProperties ? 'object' : undefined) ||
|
||||
(schemaLike.items ? 'array' : undefined) ||
|
||||
'string';
|
||||
const fieldLabel = buildLabel(schemaLike, fieldName);
|
||||
|
||||
if (fieldType === 'object') {
|
||||
const exampleObject = {};
|
||||
|
||||
if (schemaLike.properties && typeof schemaLike.properties === 'object') {
|
||||
for (const [propertyName, propertySchema] of Object.entries(schemaLike.properties)) {
|
||||
exampleObject[propertyName] = buildExample(
|
||||
propertySchema,
|
||||
currentFile,
|
||||
propertyName,
|
||||
seenRefs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(exampleObject).length === 0 && schemaLike.additionalProperties) {
|
||||
exampleObject[`${fieldLabel}字段|string`] = `${fieldLabel}值|string`;
|
||||
}
|
||||
|
||||
if (Object.keys(exampleObject).length === 0) {
|
||||
exampleObject[`${fieldLabel}|string`] = `${fieldLabel}|string`;
|
||||
}
|
||||
|
||||
return exampleObject;
|
||||
}
|
||||
|
||||
if (fieldType === 'array') {
|
||||
const itemFieldName = fieldName.endsWith('s') ? fieldName.slice(0, -1) : `${fieldName}_item`;
|
||||
return [buildExample(schemaLike.items || {}, currentFile, itemFieldName, seenRefs)];
|
||||
}
|
||||
|
||||
return fallbackScalar(fieldLabel, fieldType);
|
||||
}
|
||||
|
||||
function httpMethods(pathItem) {
|
||||
return ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'].filter(
|
||||
(method) => pathItem && typeof pathItem[method] === 'object',
|
||||
);
|
||||
}
|
||||
|
||||
for (const filePath of filePaths.filter((file) => file !== rootFile)) {
|
||||
const documentData = docs.get(filePath);
|
||||
if (!documentData || typeof documentData !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (documentData.components && documentData.components.schemas) {
|
||||
for (const [schemaName, schemaValue] of Object.entries(documentData.components.schemas)) {
|
||||
schemaValue.example = buildExample(schemaValue, filePath, schemaName, new Set());
|
||||
}
|
||||
}
|
||||
|
||||
if (documentData.paths) {
|
||||
for (const pathItem of Object.values(documentData.paths)) {
|
||||
for (const method of httpMethods(pathItem)) {
|
||||
const operation = pathItem[method];
|
||||
|
||||
const requestContent = operation?.requestBody?.content;
|
||||
if (requestContent && typeof requestContent === 'object') {
|
||||
for (const media of Object.values(requestContent)) {
|
||||
if (media && media.schema) {
|
||||
delete media.examples;
|
||||
media.example = buildExample(media.schema, filePath, 'request', new Set());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const responses = operation?.responses;
|
||||
if (responses && typeof responses === 'object') {
|
||||
for (const response of Object.values(responses)) {
|
||||
const responseContent = response?.content;
|
||||
if (!responseContent || typeof responseContent !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const media of Object.values(responseContent)) {
|
||||
if (media && media.schema) {
|
||||
delete media.examples;
|
||||
media.example = buildExample(media.schema, filePath, 'response', new Set());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, YAML.stringify(documentData, { lineWidth: 0 }), 'utf8');
|
||||
}
|
||||
|
||||
console.log(`Updated examples in ${filePaths.length - 1} split OpenAPI files.`);
|
||||
Reference in New Issue
Block a user