feat: 实现微信小程序后端接口与用户认证系统
新增微信登录/注册合一接口、资料完善接口和token刷新接口 重构用户服务层,支持自动维护用户类型和资料完整度 引入JWT认证中间件和请求验证中间件 更新文档与测试用例,支持dist构建部署
This commit is contained in:
23
back-end/dist/.env
vendored
Normal file
23
back-end/dist/.env
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Server Configuration
|
||||
PORT=3002
|
||||
|
||||
# Environment
|
||||
NODE_ENV=development
|
||||
|
||||
# API Configuration
|
||||
API_PREFIX=/api
|
||||
APP_PROTOCOL=https
|
||||
APP_DOMAIN=bai-api.blv-oa.com
|
||||
APP_BASE_URL=https://bai-api.blv-oa.com
|
||||
|
||||
# Database Configuration (Pocketbase)
|
||||
POCKETBASE_API_URL=https://bai-api.blv-oa.com/pb/
|
||||
POCKETBASE_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NzQzNjEzMzIsImlkIjoiazQ0aHI5MW90bnBydG10IiwicmVmcmVzaGFibGUiOmZhbHNlLCJ0eXBlIjoiYXV0aCJ9.qm4E6xYrDbEpAfdxZnHHRZs_EqiwHgDIIwSBz2k90Nk
|
||||
|
||||
# WeChat Configuration
|
||||
WECHAT_APPID=wx3bd7a7b19679da7a
|
||||
WECHAT_SECRET=57e40438c2a9151257b1927674db10e1
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
|
||||
JWT_EXPIRES_IN=2h
|
||||
13
back-end/dist/eslint.config.js
vendored
Normal file
13
back-end/dist/eslint.config.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = [
|
||||
{
|
||||
files: ['src/**/*.js', 'tests/**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'commonjs',
|
||||
},
|
||||
rules: {
|
||||
semi: ['error', 'never'],
|
||||
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
},
|
||||
]
|
||||
3436
back-end/dist/package-lock.json
generated
vendored
Normal file
3436
back-end/dist/package-lock.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
32
back-end/dist/package.json
vendored
Normal file
32
back-end/dist/package.json
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "web-bai-manage-api-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend API server for BAI Management System",
|
||||
"main": "dist/src/index.js",
|
||||
"scripts": {
|
||||
"dev": "node src/index.js",
|
||||
"build": "node scripts/build.js",
|
||||
"start": "node dist/src/index.js",
|
||||
"prestart": "node -e \"require('fs').accessSync('dist/src/index.js')\"",
|
||||
"test": "node --test tests/**/*.test.js",
|
||||
"lint": "eslint src tests --ext .js",
|
||||
"spec:lint": "npx @fission-ai/openspec lint spec/",
|
||||
"spec:validate": "npx @fission-ai/openspec validate spec/"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"express": "^4.18.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fission-ai/openspec": "^1.0.0",
|
||||
"eslint": "^9.23.0",
|
||||
"supertest": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
377
back-end/dist/spec/openapi.yaml
vendored
Normal file
377
back-end/dist/spec/openapi.yaml
vendored
Normal file
@@ -0,0 +1,377 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: BAI Management API
|
||||
description: BAI 管理系统后端 API 文档
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: https://bai-api.blv-oa.com
|
||||
description: BAI-api生产环境
|
||||
- url: http://localhost:3000
|
||||
description: BAI-api本地开发环境
|
||||
tags:
|
||||
- name: 系统
|
||||
description: 基础健康检查接口
|
||||
- name: 微信小程序用户
|
||||
description: 微信小程序注册、登录与鉴权接口
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
schemas:
|
||||
ApiResponse:
|
||||
type: object
|
||||
required: [code, msg, data]
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 业务状态码
|
||||
example: 200
|
||||
msg:
|
||||
type: string
|
||||
description: 响应消息
|
||||
example: 操作成功
|
||||
data:
|
||||
type: object
|
||||
description: 响应数据
|
||||
additionalProperties: true
|
||||
HealthData:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: healthy
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
HelloWorldData:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
example: Hello, World!
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
status:
|
||||
type: string
|
||||
example: success
|
||||
WechatProfileRequest:
|
||||
type: object
|
||||
required: [users_name, users_phone_code, users_picture]
|
||||
properties:
|
||||
users_name:
|
||||
type: string
|
||||
description: 用户姓名
|
||||
example: 张三
|
||||
users_phone_code:
|
||||
type: string
|
||||
description: 微信手机号获取凭证 code,由后端换取真实手机号后写入 users_phone
|
||||
example: 2b7d9f2e3c4a5b6d7e8f
|
||||
users_picture:
|
||||
type: string
|
||||
description: 用户头像 URL
|
||||
example: https://example.com/avatar.png
|
||||
WechatAuthRequest:
|
||||
type: object
|
||||
required: [users_wx_code]
|
||||
properties:
|
||||
users_wx_code:
|
||||
type: string
|
||||
description: 微信小程序登录临时凭证 code
|
||||
example: 0a1b2c3d4e5f6g
|
||||
CompanyInfo:
|
||||
type: object
|
||||
properties:
|
||||
company_id:
|
||||
type: string
|
||||
example: C10001
|
||||
company_name:
|
||||
type: string
|
||||
example: 示例科技有限公司
|
||||
company_type:
|
||||
type: string
|
||||
example: 科技服务
|
||||
company_entity:
|
||||
type: string
|
||||
example: 李四
|
||||
company_usci:
|
||||
type: string
|
||||
example: 91330100XXXXXXXXXX
|
||||
company_nationality:
|
||||
type: string
|
||||
example: 中国
|
||||
company_province:
|
||||
type: string
|
||||
example: 浙江省
|
||||
company_city:
|
||||
type: string
|
||||
example: 杭州市
|
||||
company_postalcode:
|
||||
type: string
|
||||
example: "310000"
|
||||
company_add:
|
||||
type: string
|
||||
example: 某某大道 100 号
|
||||
company_status:
|
||||
type: string
|
||||
example: 正常
|
||||
company_level:
|
||||
type: string
|
||||
example: A
|
||||
company_remark:
|
||||
type: string
|
||||
example: 重点合作客户
|
||||
UserInfo:
|
||||
type: object
|
||||
properties:
|
||||
users_id:
|
||||
type: string
|
||||
example: U202603190001
|
||||
users_type:
|
||||
type: string
|
||||
description: |
|
||||
用户类型。
|
||||
- `游客`:仅完成微信新号注册,尚未首次完整补充 `users_name`、`users_phone`、`users_picture`
|
||||
- `注册用户`:用户曾从“三项资料均为空”首次补充为“三项资料均完整”
|
||||
enum: [游客, 注册用户]
|
||||
example: 游客
|
||||
users_name:
|
||||
type: string
|
||||
example: 张三
|
||||
users_phone:
|
||||
type: string
|
||||
example: "13800138000"
|
||||
users_phone_masked:
|
||||
type: string
|
||||
example: "138****8000"
|
||||
users_picture:
|
||||
type: string
|
||||
example: https://example.com/avatar.png
|
||||
users_wx_openid:
|
||||
type: string
|
||||
example: oAbCdEfGh123456789
|
||||
company_id:
|
||||
type: string
|
||||
example: C10001
|
||||
company:
|
||||
$ref: '#/components/schemas/CompanyInfo'
|
||||
pb_id:
|
||||
type: string
|
||||
description: PocketBase 记录 id
|
||||
example: abc123xyz
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
updated:
|
||||
type: string
|
||||
format: date-time
|
||||
WechatProfileResponseData:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: 信息编辑状态
|
||||
enum: [update_success, update_failed]
|
||||
user:
|
||||
$ref: '#/components/schemas/UserInfo'
|
||||
WechatAuthResponseData:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: 登录/注册结果状态
|
||||
enum: [register_success, login_success]
|
||||
is_info_complete:
|
||||
type: boolean
|
||||
description: 用户资料是否已完善
|
||||
example: true
|
||||
token:
|
||||
type: string
|
||||
description: JWT 令牌
|
||||
user:
|
||||
$ref: '#/components/schemas/UserInfo'
|
||||
paths:
|
||||
/api/test-helloworld:
|
||||
post:
|
||||
tags: [系统]
|
||||
summary: Test endpoint
|
||||
description: Returns a hello world message
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/HelloWorldData'
|
||||
/api/health:
|
||||
post:
|
||||
tags: [系统]
|
||||
summary: 健康检查
|
||||
description: 检查服务是否正常运行
|
||||
responses:
|
||||
'200':
|
||||
description: 服务状态正常
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/HealthData'
|
||||
/api/wechat/login:
|
||||
post:
|
||||
tags: [微信小程序用户]
|
||||
summary: 微信小程序登录/注册合一
|
||||
description: |
|
||||
使用微信小程序临时登录凭证换取 openid。
|
||||
若用户不存在,则自动创建新账号并返回完整用户信息;若已存在,则直接登录并返回完整用户信息。
|
||||
登录/注册合一接口不处理手机号获取。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- users_wx_code
|
||||
properties:
|
||||
users_wx_code:
|
||||
type: string
|
||||
description: 微信小程序登录临时凭证 code
|
||||
example: 0a1b2c3d4e5f6g
|
||||
responses:
|
||||
'200':
|
||||
description: 登录或注册成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/WechatAuthResponseData'
|
||||
'400':
|
||||
description: 参数错误
|
||||
'415':
|
||||
description: 请求体格式错误,仅支持 application/json
|
||||
'429':
|
||||
description: 重复提交过于频繁
|
||||
'500':
|
||||
description: 服务端异常
|
||||
/api/wechat/profile:
|
||||
post:
|
||||
tags: [微信小程序用户]
|
||||
summary: 微信小程序用户信息编辑
|
||||
description: |
|
||||
从请求头 `users_wx_openid` 定位用户,不再从 body 中传 `users_wx_code`。
|
||||
body 中传入 `users_name`、`users_phone_code`、`users_picture`。
|
||||
后端会通过微信 `users_phone_code` 调用官方接口换取真实手机号,再写入 `users_phone`。
|
||||
`users_name`、`users_phone_code`、`users_picture` 均为必填项。
|
||||
当且仅当用户原先这三项资料全部为空,且本次首次完整补充三项资料时,系统自动将 `users_type` 从 `游客` 更新为 `注册用户`。
|
||||
后续资料修改不会再影响已确定的 `users_type`。
|
||||
返回更新后的完整用户信息,其中手机号等敏感字段需脱敏处理。
|
||||
parameters:
|
||||
- in: header
|
||||
name: users_wx_openid
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: 微信用户唯一标识,用于数据库查询
|
||||
- in: header
|
||||
name: Authorization
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: 标准 JWT 认证头,格式为 `Bearer <token>`
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- users_name
|
||||
- users_phone_code
|
||||
- users_picture
|
||||
properties:
|
||||
users_name:
|
||||
type: string
|
||||
description: 用户姓名
|
||||
example: 张三
|
||||
users_phone_code:
|
||||
type: string
|
||||
description: 微信手机号获取凭证 code
|
||||
example: 2b7d9f2e3c4a5b6d7e8f
|
||||
users_picture:
|
||||
type: string
|
||||
description: 用户头像 URL
|
||||
example: https://example.com/avatar.png
|
||||
responses:
|
||||
'200':
|
||||
description: 信息更新成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/WechatProfileResponseData'
|
||||
'400':
|
||||
description: 参数错误
|
||||
'401':
|
||||
description: 请求头缺少 users_wx_openid 或 Authorization,或令牌无效
|
||||
'415':
|
||||
description: 请求体格式错误,仅支持 application/json
|
||||
'404':
|
||||
description: 用户不存在
|
||||
'500':
|
||||
description: 服务端异常
|
||||
/api/wechat/refresh-token:
|
||||
post:
|
||||
tags: [微信小程序用户]
|
||||
summary: 微信小程序刷新 token
|
||||
description: |
|
||||
小程序通过请求头中的 `users_wx_openid` 定位用户,并返回新的 JWT token。
|
||||
本接口无 body 参数,请求方法固定为 POST。
|
||||
本接口不要求旧 `Authorization`,仅依赖 `users_wx_openid` 识别用户并签发新 token。
|
||||
parameters:
|
||||
- in: header
|
||||
name: users_wx_openid
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: 微信用户唯一标识,用于数据库查询
|
||||
responses:
|
||||
'200':
|
||||
description: 刷新成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: 新的 JWT 令牌
|
||||
'404':
|
||||
description: 未注册用户
|
||||
'401':
|
||||
description: 请求头缺少 users_wx_openid
|
||||
'500':
|
||||
description: 服务端异常
|
||||
28
back-end/dist/src/config/env.js
vendored
Normal file
28
back-end/dist/src/config/env.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
const dotenv = require('dotenv')
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const requiredEnv = ['WECHAT_APPID', 'WECHAT_SECRET', 'JWT_SECRET']
|
||||
|
||||
for (const key of requiredEnv) {
|
||||
if (!process.env[key]) {
|
||||
console.warn(`[config] 缺少环境变量: ${key}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
port: Number(process.env.PORT || 3000),
|
||||
apiPrefix: process.env.API_PREFIX || '/api',
|
||||
appProtocol: process.env.APP_PROTOCOL || 'http',
|
||||
appDomain: process.env.APP_DOMAIN || 'localhost',
|
||||
appBaseUrl:
|
||||
process.env.APP_BASE_URL
|
||||
|| `${process.env.APP_PROTOCOL || 'http'}://${process.env.APP_DOMAIN || 'localhost'}${process.env.PORT ? `:${process.env.PORT}` : ''}`,
|
||||
pocketbaseUrl: process.env.POCKETBASE_API_URL || '',
|
||||
pocketbaseAuthToken: process.env.POCKETBASE_AUTH_TOKEN || '',
|
||||
wechatAppId: process.env.WECHAT_APPID || '',
|
||||
wechatSecret: process.env.WECHAT_SECRET || '',
|
||||
jwtSecret: process.env.JWT_SECRET || 'change_me',
|
||||
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '2h',
|
||||
}
|
||||
36
back-end/dist/src/controllers/wechatController.js
vendored
Normal file
36
back-end/dist/src/controllers/wechatController.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
const asyncHandler = require('../utils/asyncHandler')
|
||||
const { success } = require('../utils/response')
|
||||
const {
|
||||
validateLoginBody,
|
||||
validateProfileEditBody,
|
||||
} = require('../middlewares/validateWechatAuth')
|
||||
const userService = require('../services/userService')
|
||||
|
||||
const login = asyncHandler(async (req, res) => {
|
||||
const payload = validateLoginBody(req.body)
|
||||
const data = await userService.authenticateWechatUser(payload)
|
||||
|
||||
const messageMap = {
|
||||
register_success: '注册成功',
|
||||
login_success: '登录成功',
|
||||
}
|
||||
|
||||
return success(res, messageMap[data.status] || '登录成功', data)
|
||||
})
|
||||
|
||||
const updateProfile = asyncHandler(async (req, res) => {
|
||||
const payload = validateProfileEditBody(req.body)
|
||||
const data = await userService.updateWechatUserProfile(req.usersWxOpenid, payload)
|
||||
return success(res, '信息更新成功', data)
|
||||
})
|
||||
|
||||
const refreshToken = asyncHandler(async (req, res) => {
|
||||
const data = await userService.refreshWechatToken(req.usersWxOpenid)
|
||||
return success(res, '刷新成功', data)
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
updateProfile,
|
||||
login,
|
||||
refreshToken,
|
||||
}
|
||||
39
back-end/dist/src/index.js
vendored
Normal file
39
back-end/dist/src/index.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
const express = require('express')
|
||||
const env = require('./config/env')
|
||||
const { fail } = require('./utils/response')
|
||||
const requestLogger = require('./middlewares/requestLogger')
|
||||
const errorHandler = require('./middlewares/errorHandler')
|
||||
const apiRoutes = require('./routes/apiRoutes')
|
||||
|
||||
function createApp() {
|
||||
const app = express()
|
||||
|
||||
app.use(express.json({ limit: '1mb' }))
|
||||
app.use(requestLogger)
|
||||
|
||||
app.use(env.apiPrefix, apiRoutes)
|
||||
|
||||
app.use((req, res) => {
|
||||
return fail(res, 404, 'Route not found', {
|
||||
path: req.path,
|
||||
})
|
||||
})
|
||||
|
||||
app.use(errorHandler)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const app = createApp()
|
||||
app.listen(env.port, () => {
|
||||
console.log(`Server running on port ${env.port}`)
|
||||
console.log(`Public base URL: ${env.appBaseUrl}`)
|
||||
console.log(`Test endpoint: ${env.appBaseUrl}${env.apiPrefix}/test-helloworld`)
|
||||
console.log(`Health check: ${env.appBaseUrl}${env.apiPrefix}/health`)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createApp,
|
||||
}
|
||||
22
back-end/dist/src/middlewares/duplicateGuard.js
vendored
Normal file
22
back-end/dist/src/middlewares/duplicateGuard.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
const AppError = require('../utils/appError')
|
||||
|
||||
const requestCache = new Map()
|
||||
const WINDOW_MS = 5000
|
||||
|
||||
module.exports = function duplicateGuard(req, res, next) {
|
||||
const key = `${req.ip}:${req.originalUrl}:${JSON.stringify(req.body || {})}`
|
||||
const now = Date.now()
|
||||
const lastTime = requestCache.get(key)
|
||||
|
||||
if (lastTime && now - lastTime < WINDOW_MS) {
|
||||
return next(new AppError('请求过于频繁,请稍后重试', 429))
|
||||
}
|
||||
|
||||
requestCache.set(key, now)
|
||||
|
||||
setTimeout(() => {
|
||||
requestCache.delete(key)
|
||||
}, WINDOW_MS)
|
||||
|
||||
next()
|
||||
}
|
||||
19
back-end/dist/src/middlewares/errorHandler.js
vendored
Normal file
19
back-end/dist/src/middlewares/errorHandler.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
const { fail } = require('../utils/response')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
module.exports = function errorHandler(err, req, res, next) {
|
||||
logger.error('接口处理异常', {
|
||||
path: req.originalUrl,
|
||||
method: req.method,
|
||||
error: err.message,
|
||||
details: err.details || {},
|
||||
})
|
||||
|
||||
if (res.headersSent) {
|
||||
return next(err)
|
||||
}
|
||||
|
||||
return fail(res, err.statusCode || 500, err.message || '服务器内部错误', {
|
||||
...(err.details || {}),
|
||||
})
|
||||
}
|
||||
20
back-end/dist/src/middlewares/jwtAuth.js
vendored
Normal file
20
back-end/dist/src/middlewares/jwtAuth.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const env = require('../config/env')
|
||||
const AppError = require('../utils/appError')
|
||||
|
||||
module.exports = function jwtAuth(req, res, next) {
|
||||
const authHeader = req.headers.authorization || ''
|
||||
const [scheme, token] = authHeader.split(' ')
|
||||
|
||||
if (scheme !== 'Bearer' || !token) {
|
||||
return next(new AppError('未提供有效的认证令牌', 401))
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, env.jwtSecret)
|
||||
req.user = decoded
|
||||
next()
|
||||
} catch {
|
||||
next(new AppError('认证令牌无效或已过期', 401))
|
||||
}
|
||||
}
|
||||
17
back-end/dist/src/middlewares/requestLogger.js
vendored
Normal file
17
back-end/dist/src/middlewares/requestLogger.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
module.exports = function requestLogger(req, res, next) {
|
||||
const start = Date.now()
|
||||
|
||||
res.on('finish', () => {
|
||||
logger.info('请求完成', {
|
||||
method: req.method,
|
||||
path: req.originalUrl,
|
||||
statusCode: res.statusCode,
|
||||
durationMs: Date.now() - start,
|
||||
ip: req.ip,
|
||||
})
|
||||
})
|
||||
|
||||
next()
|
||||
}
|
||||
15
back-end/dist/src/middlewares/requireJsonBody.js
vendored
Normal file
15
back-end/dist/src/middlewares/requireJsonBody.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
const AppError = require('../utils/appError')
|
||||
|
||||
module.exports = function requireJsonBody(req, res, next) {
|
||||
const methods = ['POST', 'PUT', 'PATCH']
|
||||
|
||||
if (!methods.includes(req.method)) {
|
||||
return next()
|
||||
}
|
||||
|
||||
if (!req.is('application/json')) {
|
||||
return next(new AppError('请求体必须为 application/json', 415))
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
12
back-end/dist/src/middlewares/requireWechatOpenid.js
vendored
Normal file
12
back-end/dist/src/middlewares/requireWechatOpenid.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
const AppError = require('../utils/appError')
|
||||
|
||||
module.exports = function requireWechatOpenid(req, res, next) {
|
||||
const usersWxOpenid = req.headers['users_wx_openid']
|
||||
|
||||
if (!usersWxOpenid) {
|
||||
return next(new AppError('请求头缺少 users_wx_openid', 401))
|
||||
}
|
||||
|
||||
req.usersWxOpenid = usersWxOpenid
|
||||
next()
|
||||
}
|
||||
35
back-end/dist/src/middlewares/validateWechatAuth.js
vendored
Normal file
35
back-end/dist/src/middlewares/validateWechatAuth.js
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
const AppError = require('../utils/appError')
|
||||
const { sanitizePayload } = require('../utils/sanitize')
|
||||
|
||||
function validateLoginBody(body = {}) {
|
||||
const payload = sanitizePayload(body)
|
||||
|
||||
if (!payload.users_wx_code) {
|
||||
throw new AppError('users_wx_code 为必填项', 400)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function validateProfileEditBody(body = {}) {
|
||||
const payload = sanitizePayload(body)
|
||||
|
||||
if (!payload.users_name) {
|
||||
throw new AppError('users_name 为必填项', 400)
|
||||
}
|
||||
|
||||
if (!payload.users_phone_code) {
|
||||
throw new AppError('users_phone_code 为必填项', 400)
|
||||
}
|
||||
|
||||
if (!payload.users_picture) {
|
||||
throw new AppError('users_picture 为必填项', 400)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateLoginBody,
|
||||
validateProfileEditBody,
|
||||
}
|
||||
31
back-end/dist/src/middlewares/wechatHeadersAuth.js
vendored
Normal file
31
back-end/dist/src/middlewares/wechatHeadersAuth.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const env = require('../config/env')
|
||||
const AppError = require('../utils/appError')
|
||||
const requireWechatOpenid = require('./requireWechatOpenid')
|
||||
|
||||
module.exports = function wechatHeadersAuth(req, res, next) {
|
||||
requireWechatOpenid(req, res, (openidError) => {
|
||||
if (openidError) {
|
||||
return next(openidError)
|
||||
}
|
||||
|
||||
const usersWxOpenid = req.usersWxOpenid
|
||||
const authHeader = req.headers.authorization || ''
|
||||
const [scheme, token] = authHeader.split(' ')
|
||||
|
||||
if (scheme !== 'Bearer' || !token) {
|
||||
return next(new AppError('请求头缺少 Authorization', 401))
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, env.jwtSecret)
|
||||
if (decoded.users_wx_openid && decoded.users_wx_openid !== usersWxOpenid) {
|
||||
return next(new AppError('请求头中的 users_wx_openid 与令牌不匹配', 401))
|
||||
}
|
||||
req.user = decoded
|
||||
next()
|
||||
} catch {
|
||||
next(new AppError('认证令牌无效或已过期', 401))
|
||||
}
|
||||
})
|
||||
}
|
||||
24
back-end/dist/src/routes/apiRoutes.js
vendored
Normal file
24
back-end/dist/src/routes/apiRoutes.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
const express = require('express')
|
||||
const { success } = require('../utils/response')
|
||||
const wechatRoutes = require('./wechatRoutes')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/test-helloworld', (req, res) => {
|
||||
return success(res, '请求成功', {
|
||||
message: 'Hello, World!',
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/health', (req, res) => {
|
||||
return success(res, '服务运行正常', {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
})
|
||||
|
||||
router.use('/wechat', wechatRoutes)
|
||||
|
||||
module.exports = router
|
||||
14
back-end/dist/src/routes/wechatRoutes.js
vendored
Normal file
14
back-end/dist/src/routes/wechatRoutes.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
const express = require('express')
|
||||
const duplicateGuard = require('../middlewares/duplicateGuard')
|
||||
const requireJsonBody = require('../middlewares/requireJsonBody')
|
||||
const requireWechatOpenid = require('../middlewares/requireWechatOpenid')
|
||||
const wechatHeadersAuth = require('../middlewares/wechatHeadersAuth')
|
||||
const controller = require('../controllers/wechatController')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/login', requireJsonBody, duplicateGuard, controller.login)
|
||||
router.post('/profile', requireJsonBody, wechatHeadersAuth, duplicateGuard, controller.updateProfile)
|
||||
router.post('/refresh-token', requireWechatOpenid, controller.refreshToken)
|
||||
|
||||
module.exports = router
|
||||
12
back-end/dist/src/services/jwtService.js
vendored
Normal file
12
back-end/dist/src/services/jwtService.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const env = require('../config/env')
|
||||
|
||||
function signAccessToken(payload) {
|
||||
return jwt.sign(payload, env.jwtSecret, {
|
||||
expiresIn: env.jwtExpiresIn,
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
signAccessToken,
|
||||
}
|
||||
90
back-end/dist/src/services/pocketbaseService.js
vendored
Normal file
90
back-end/dist/src/services/pocketbaseService.js
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
const axios = require('axios')
|
||||
const env = require('../config/env')
|
||||
const AppError = require('../utils/appError')
|
||||
|
||||
function getHeaders() {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (env.pocketbaseAuthToken) {
|
||||
headers.Authorization = env.pocketbaseAuthToken.startsWith('Bearer ')
|
||||
? env.pocketbaseAuthToken
|
||||
: `Bearer ${env.pocketbaseAuthToken}`
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
function buildUrl(path) {
|
||||
const base = env.pocketbaseUrl.replace(/\/+$/, '')
|
||||
const normalizedPath = path.replace(/^\/+/, '')
|
||||
return `${base}/${normalizedPath}`
|
||||
}
|
||||
|
||||
async function request(config) {
|
||||
try {
|
||||
const response = await axios({
|
||||
timeout: 10000,
|
||||
headers: getHeaders(),
|
||||
...config,
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const detail = error.response?.data || error.message
|
||||
throw new AppError('PocketBase 数据操作失败', 500, {
|
||||
detail,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function listUsersByFilter(filter) {
|
||||
const data = await request({
|
||||
method: 'get',
|
||||
url: buildUrl('collections/tbl_users/records'),
|
||||
params: {
|
||||
filter,
|
||||
perPage: 1,
|
||||
},
|
||||
})
|
||||
|
||||
return data.items || []
|
||||
}
|
||||
|
||||
async function createUser(payload) {
|
||||
return request({
|
||||
method: 'post',
|
||||
url: buildUrl('collections/tbl_users/records'),
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
async function updateUser(recordId, payload) {
|
||||
return request({
|
||||
method: 'patch',
|
||||
url: buildUrl(`collections/tbl_users/records/${recordId}`),
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
async function getCompanyByCompanyId(companyId) {
|
||||
if (!companyId) return null
|
||||
|
||||
const data = await request({
|
||||
method: 'get',
|
||||
url: buildUrl('collections/tbl_company/records'),
|
||||
params: {
|
||||
filter: `company_id = "${companyId}"`,
|
||||
perPage: 1,
|
||||
},
|
||||
})
|
||||
|
||||
return data.items?.[0] || null
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listUsersByFilter,
|
||||
createUser,
|
||||
updateUser,
|
||||
getCompanyByCompanyId,
|
||||
}
|
||||
208
back-end/dist/src/services/userService.js
vendored
Normal file
208
back-end/dist/src/services/userService.js
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
const crypto = require('crypto')
|
||||
const AppError = require('../utils/appError')
|
||||
const logger = require('../utils/logger')
|
||||
const wechatService = require('./wechatService')
|
||||
const jwtService = require('./jwtService')
|
||||
const pocketbaseService = require('./pocketbaseService')
|
||||
|
||||
const userMutationLocks = new Map()
|
||||
const GUEST_USER_TYPE = '游客'
|
||||
const REGISTERED_USER_TYPE = '注册用户'
|
||||
|
||||
function buildUserId() {
|
||||
const now = new Date()
|
||||
const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`
|
||||
const suffix = crypto.randomInt(1000, 9999)
|
||||
return `U${date}${suffix}`
|
||||
}
|
||||
|
||||
async function enrichUser(user) {
|
||||
const company = await pocketbaseService.getCompanyByCompanyId(user.company_id)
|
||||
return {
|
||||
pb_id: user.id,
|
||||
users_id: user.users_id,
|
||||
users_type: user.users_type || GUEST_USER_TYPE,
|
||||
users_name: user.users_name,
|
||||
users_phone: user.users_phone,
|
||||
users_phone_masked: maskPhone(user.users_phone),
|
||||
users_picture: user.users_picture,
|
||||
users_wx_openid: user.users_wx_openid,
|
||||
company_id: user.company_id || '',
|
||||
company,
|
||||
created: user.created,
|
||||
updated: user.updated,
|
||||
raw: user,
|
||||
}
|
||||
}
|
||||
|
||||
async function findUserByOpenid(usersWxOpenid) {
|
||||
const users = await pocketbaseService.listUsersByFilter(`users_wx_openid = "${usersWxOpenid}"`)
|
||||
return users[0] || null
|
||||
}
|
||||
|
||||
function maskPhone(phone = '') {
|
||||
if (!phone || phone.length < 7) return ''
|
||||
return `${phone.slice(0, 3)}****${phone.slice(-4)}`
|
||||
}
|
||||
|
||||
function isInfoComplete(user = {}) {
|
||||
return Boolean(user.users_name && user.users_phone && user.users_picture)
|
||||
}
|
||||
|
||||
function isAllProfileFieldsEmpty(user = {}) {
|
||||
return !user.users_name && !user.users_phone && !user.users_picture
|
||||
}
|
||||
|
||||
async function withUserLock(lockKey, handler) {
|
||||
const previous = userMutationLocks.get(lockKey) || Promise.resolve()
|
||||
let release
|
||||
const current = new Promise((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
|
||||
userMutationLocks.set(lockKey, previous.then(() => current))
|
||||
|
||||
await previous
|
||||
|
||||
try {
|
||||
return await handler()
|
||||
} finally {
|
||||
release()
|
||||
if (userMutationLocks.get(lockKey) === current) {
|
||||
userMutationLocks.delete(lockKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateWechatUser(payload) {
|
||||
const openid = await wechatService.getWxOpenId(payload.users_wx_code)
|
||||
return withUserLock(`auth:${openid}`, async () => {
|
||||
const existing = await findUserByOpenid(openid)
|
||||
|
||||
if (existing) {
|
||||
logger.warn('微信注册命中已存在账号', {
|
||||
users_wx_openid: openid,
|
||||
users_type: existing.users_type || GUEST_USER_TYPE,
|
||||
})
|
||||
|
||||
const user = await enrichUser(existing)
|
||||
const token = jwtService.signAccessToken({
|
||||
users_id: user.users_id,
|
||||
users_wx_openid: user.users_wx_openid,
|
||||
})
|
||||
|
||||
return {
|
||||
status: 'login_success',
|
||||
is_info_complete: isInfoComplete(existing),
|
||||
token,
|
||||
user,
|
||||
}
|
||||
}
|
||||
|
||||
const created = await pocketbaseService.createUser({
|
||||
users_id: buildUserId(),
|
||||
users_wx_openid: openid,
|
||||
users_type: GUEST_USER_TYPE,
|
||||
})
|
||||
|
||||
const user = await enrichUser(created)
|
||||
const token = jwtService.signAccessToken({
|
||||
users_id: user.users_id,
|
||||
users_wx_openid: user.users_wx_openid,
|
||||
})
|
||||
|
||||
logger.info('微信用户注册成功', {
|
||||
users_id: user.users_id,
|
||||
users_phone: user.users_phone,
|
||||
users_type: user.users_type,
|
||||
})
|
||||
|
||||
return {
|
||||
status: 'register_success',
|
||||
is_info_complete: false,
|
||||
token,
|
||||
user,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function updateWechatUserProfile(usersWxOpenid, payload) {
|
||||
return withUserLock(`profile:${usersWxOpenid}`, async () => {
|
||||
const currentUser = await findUserByOpenid(usersWxOpenid)
|
||||
|
||||
if (!currentUser) {
|
||||
throw new AppError('未找到待编辑的用户', 404)
|
||||
}
|
||||
|
||||
const usersPhone = await wechatService.getWxPhoneNumber(payload.users_phone_code)
|
||||
|
||||
if (usersPhone && usersPhone !== currentUser.users_phone) {
|
||||
const samePhoneUsers = await pocketbaseService.listUsersByFilter(`users_phone = "${usersPhone}"`)
|
||||
const phoneUsedByOther = samePhoneUsers.some((item) => item.id !== currentUser.id)
|
||||
if (phoneUsedByOther) {
|
||||
throw new AppError('手机号已被注册', 400)
|
||||
}
|
||||
}
|
||||
|
||||
const shouldPromoteUserType =
|
||||
isAllProfileFieldsEmpty(currentUser)
|
||||
&& payload.users_name
|
||||
&& usersPhone
|
||||
&& payload.users_picture
|
||||
&& (currentUser.users_type === GUEST_USER_TYPE || !currentUser.users_type)
|
||||
|
||||
const updatePayload = {
|
||||
users_name: payload.users_name,
|
||||
users_phone: usersPhone,
|
||||
users_picture: payload.users_picture,
|
||||
}
|
||||
|
||||
if (shouldPromoteUserType) {
|
||||
updatePayload.users_type = REGISTERED_USER_TYPE
|
||||
}
|
||||
|
||||
const updated = await pocketbaseService.updateUser(currentUser.id, updatePayload)
|
||||
const user = await enrichUser(updated)
|
||||
|
||||
logger.info('微信用户资料更新成功', {
|
||||
users_id: user.users_id,
|
||||
users_phone: user.users_phone,
|
||||
users_type_before: currentUser.users_type || GUEST_USER_TYPE,
|
||||
users_type_after: user.users_type,
|
||||
users_type_promoted: shouldPromoteUserType,
|
||||
})
|
||||
|
||||
return {
|
||||
status: 'update_success',
|
||||
user,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshWechatToken(usersWxOpenid) {
|
||||
const userRecord = await findUserByOpenid(usersWxOpenid)
|
||||
|
||||
if (!userRecord) {
|
||||
throw new AppError('未注册用户', 404)
|
||||
}
|
||||
|
||||
const token = jwtService.signAccessToken({
|
||||
users_id: userRecord.users_id,
|
||||
users_wx_openid: userRecord.users_wx_openid,
|
||||
})
|
||||
|
||||
logger.info('微信用户刷新令牌成功', {
|
||||
users_id: userRecord.users_id,
|
||||
users_wx_openid: userRecord.users_wx_openid,
|
||||
})
|
||||
|
||||
return {
|
||||
token,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticateWechatUser,
|
||||
updateWechatUserProfile,
|
||||
refreshWechatToken,
|
||||
}
|
||||
117
back-end/dist/src/services/wechatService.js
vendored
Normal file
117
back-end/dist/src/services/wechatService.js
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
const axios = require('axios')
|
||||
const env = require('../config/env')
|
||||
const AppError = require('../utils/appError')
|
||||
|
||||
let accessTokenCache = {
|
||||
token: '',
|
||||
expiresAt: 0,
|
||||
}
|
||||
|
||||
async function getWxOpenId(code) {
|
||||
if (!env.wechatAppId || !env.wechatSecret) {
|
||||
throw new AppError('微信小程序配置缺失', 500)
|
||||
}
|
||||
|
||||
try {
|
||||
const url = 'https://api.weixin.qq.com/sns/jscode2session'
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
appid: env.wechatAppId,
|
||||
secret: env.wechatSecret,
|
||||
js_code: code,
|
||||
grant_type: 'authorization_code',
|
||||
},
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
const data = response.data || {}
|
||||
|
||||
if (data.openid) {
|
||||
return data.openid
|
||||
}
|
||||
|
||||
if (data.errcode || data.errmsg) {
|
||||
throw new AppError(`获取OpenID失败: ${data.errcode || ''} ${data.errmsg || ''}`.trim(), 502, {
|
||||
wechat: data,
|
||||
})
|
||||
}
|
||||
|
||||
throw new AppError('获取OpenID失败: 响应中未包含openid', 502)
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) throw error
|
||||
throw new AppError(`获取微信OpenID时发生错误: ${error.message}`, 502)
|
||||
}
|
||||
}
|
||||
|
||||
async function getWechatAccessToken() {
|
||||
if (accessTokenCache.token && Date.now() < accessTokenCache.expiresAt) {
|
||||
return accessTokenCache.token
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('https://api.weixin.qq.com/cgi-bin/token', {
|
||||
params: {
|
||||
grant_type: 'client_credential',
|
||||
appid: env.wechatAppId,
|
||||
secret: env.wechatSecret,
|
||||
},
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
const data = response.data || {}
|
||||
|
||||
if (!data.access_token) {
|
||||
throw new AppError(`获取微信 access_token 失败: ${data.errcode || ''} ${data.errmsg || ''}`.trim(), 502, {
|
||||
wechat: data,
|
||||
})
|
||||
}
|
||||
|
||||
accessTokenCache = {
|
||||
token: data.access_token,
|
||||
expiresAt: Date.now() + Math.max((data.expires_in || 7200) - 300, 60) * 1000,
|
||||
}
|
||||
|
||||
return accessTokenCache.token
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) throw error
|
||||
throw new AppError(`获取微信 access_token 时发生错误: ${error.message}`, 502)
|
||||
}
|
||||
}
|
||||
|
||||
async function getWxPhoneNumber(phoneCode) {
|
||||
const accessToken = await getWechatAccessToken()
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=${accessToken}`,
|
||||
{
|
||||
code: phoneCode,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 10000,
|
||||
}
|
||||
)
|
||||
|
||||
const data = response.data || {}
|
||||
const phone = data.phone_info?.purePhoneNumber || data.phone_info?.phoneNumber
|
||||
|
||||
if (phone) {
|
||||
return phone
|
||||
}
|
||||
|
||||
throw new AppError(`获取微信手机号失败: ${data.errcode || ''} ${data.errmsg || ''}`.trim(), 502, {
|
||||
wechat: data,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) throw error
|
||||
throw new AppError(`获取微信手机号时发生错误: ${error.message}`, 502)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWxOpenId,
|
||||
getWxPhoneNumber,
|
||||
}
|
||||
10
back-end/dist/src/utils/appError.js
vendored
Normal file
10
back-end/dist/src/utils/appError.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode = 500, details = {}) {
|
||||
super(message)
|
||||
this.name = 'AppError'
|
||||
this.statusCode = statusCode
|
||||
this.details = details
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AppError
|
||||
5
back-end/dist/src/utils/asyncHandler.js
vendored
Normal file
5
back-end/dist/src/utils/asyncHandler.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = function asyncHandler(fn) {
|
||||
return function wrappedHandler(req, res, next) {
|
||||
Promise.resolve(fn(req, res, next)).catch(next)
|
||||
}
|
||||
}
|
||||
27
back-end/dist/src/utils/logger.js
vendored
Normal file
27
back-end/dist/src/utils/logger.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
function createLog(method, message, meta = {}) {
|
||||
const payload = {
|
||||
time: new Date().toISOString(),
|
||||
method,
|
||||
message,
|
||||
...meta,
|
||||
}
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
|
||||
function info(message, meta) {
|
||||
console.log(createLog('INFO', message, meta))
|
||||
}
|
||||
|
||||
function warn(message, meta) {
|
||||
console.warn(createLog('WARN', message, meta))
|
||||
}
|
||||
|
||||
function error(message, meta) {
|
||||
console.error(createLog('ERROR', message, meta))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
}
|
||||
20
back-end/dist/src/utils/response.js
vendored
Normal file
20
back-end/dist/src/utils/response.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
function success(res, msg = '操作成功', data = {}, code = 200) {
|
||||
return res.status(code).json({
|
||||
code,
|
||||
msg,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
function fail(res, code = 500, msg = '服务器内部错误', data = {}) {
|
||||
return res.status(code).json({
|
||||
code,
|
||||
msg,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
success,
|
||||
fail,
|
||||
}
|
||||
17
back-end/dist/src/utils/sanitize.js
vendored
Normal file
17
back-end/dist/src/utils/sanitize.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
function sanitizeString(value) {
|
||||
if (typeof value !== 'string') return ''
|
||||
return value.replace(/[<>\\]/g, '').trim()
|
||||
}
|
||||
|
||||
function sanitizePayload(payload = {}) {
|
||||
return Object.keys(payload).reduce((acc, key) => {
|
||||
const value = payload[key]
|
||||
acc[key] = typeof value === 'string' ? sanitizeString(value) : value
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sanitizeString,
|
||||
sanitizePayload,
|
||||
}
|
||||
Reference in New Issue
Block a user