commit a2c0d17210c60b8683b45fd225489984d11b416d Author: Jiacheng Xu Date: Tue Nov 18 09:36:58 2025 +0800 初始化项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e96e557 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/WxCheckApi/bin +/WxCheckApi/obj +/WxCheckApi/.vs +/WxCheckApi/*.user +/.vscode diff --git a/CommunicationRecords/app.js b/CommunicationRecords/app.js new file mode 100644 index 0000000..b3e01d2 --- /dev/null +++ b/CommunicationRecords/app.js @@ -0,0 +1,20 @@ +// app.js +App({ + onLaunch() { + // 展示本地存储能力 + const logs = wx.getStorageSync('logs') || [] + logs.unshift(Date.now()) + wx.setStorageSync('logs', logs) + + // 登录 + wx.login({ + success: res => { + // 发送 res.code 到后台换取 openId, sessionKey, unionId + } + }) + }, + globalData: { + userInfo: null + } +}) + diff --git a/CommunicationRecords/app.json b/CommunicationRecords/app.json new file mode 100644 index 0000000..042e029 --- /dev/null +++ b/CommunicationRecords/app.json @@ -0,0 +1,15 @@ +{ + "pages": [ + "pages/index/index", + "pages/logs/logs" + ], + "window": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "Weixin", + "navigationBarBackgroundColor": "#ffffff" + }, + "style": "v2", + "componentFramework": "glass-easel", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} diff --git a/CommunicationRecords/app.wxss b/CommunicationRecords/app.wxss new file mode 100644 index 0000000..06c6fc9 --- /dev/null +++ b/CommunicationRecords/app.wxss @@ -0,0 +1,10 @@ +/**app.wxss**/ +.container { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 200rpx 0; + box-sizing: border-box; +} diff --git a/CommunicationRecords/pages/index/index.js b/CommunicationRecords/pages/index/index.js new file mode 100644 index 0000000..a8d6aa5 --- /dev/null +++ b/CommunicationRecords/pages/index/index.js @@ -0,0 +1,49 @@ +// index.js +const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0' + +Page({ + data: { + motto: 'Hello World', + userInfo: { + avatarUrl: defaultAvatarUrl, + nickName: '', + }, + hasUserInfo: false, + canIUseGetUserProfile: wx.canIUse('getUserProfile'), + canIUseNicknameComp: wx.canIUse('input.type.nickname'), + }, + bindViewTap() { + wx.navigateTo({ + url: '../logs/logs' + }) + }, + onChooseAvatar(e) { + const { avatarUrl } = e.detail + const { nickName } = this.data.userInfo + this.setData({ + "userInfo.avatarUrl": avatarUrl, + hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl, + }) + }, + onInputChange(e) { + const nickName = e.detail.value + const { avatarUrl } = this.data.userInfo + this.setData({ + "userInfo.nickName": nickName, + hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl, + }) + }, + getUserProfile(e) { + // 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗 + wx.getUserProfile({ + desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写 + success: (res) => { + console.log(res) + this.setData({ + userInfo: res.userInfo, + hasUserInfo: true + }) + } + }) + }, +}) diff --git a/CommunicationRecords/pages/index/index.json b/CommunicationRecords/pages/index/index.json new file mode 100644 index 0000000..b55b5a2 --- /dev/null +++ b/CommunicationRecords/pages/index/index.json @@ -0,0 +1,4 @@ +{ + "usingComponents": { + } +} \ No newline at end of file diff --git a/CommunicationRecords/pages/index/index.wxml b/CommunicationRecords/pages/index/index.wxml new file mode 100644 index 0000000..0721ba0 --- /dev/null +++ b/CommunicationRecords/pages/index/index.wxml @@ -0,0 +1,27 @@ + + + + + + + + 昵称 + + + + + + 请使用2.10.4及以上版本基础库 + + + + {{userInfo.nickName}} + + + + {{motto}} + + + diff --git a/CommunicationRecords/pages/index/index.wxss b/CommunicationRecords/pages/index/index.wxss new file mode 100644 index 0000000..1ebed4b --- /dev/null +++ b/CommunicationRecords/pages/index/index.wxss @@ -0,0 +1,62 @@ +/**index.wxss**/ +page { + height: 100vh; + display: flex; + flex-direction: column; +} +.scrollarea { + flex: 1; + overflow-y: hidden; +} + +.userinfo { + display: flex; + flex-direction: column; + align-items: center; + color: #aaa; + width: 80%; +} + +.userinfo-avatar { + overflow: hidden; + width: 128rpx; + height: 128rpx; + margin: 20rpx; + border-radius: 50%; +} + +.usermotto { + margin-top: 200px; +} + +.avatar-wrapper { + padding: 0; + width: 56px !important; + border-radius: 8px; + margin-top: 40px; + margin-bottom: 40px; +} + +.avatar { + display: block; + width: 56px; + height: 56px; +} + +.nickname-wrapper { + display: flex; + width: 100%; + padding: 16px; + box-sizing: border-box; + border-top: .5px solid rgba(0, 0, 0, 0.1); + border-bottom: .5px solid rgba(0, 0, 0, 0.1); + color: black; +} + +.nickname-label { + width: 105px; +} + +.nickname-input { + flex: 1; +} diff --git a/CommunicationRecords/pages/logs/logs.js b/CommunicationRecords/pages/logs/logs.js new file mode 100644 index 0000000..85f6aac --- /dev/null +++ b/CommunicationRecords/pages/logs/logs.js @@ -0,0 +1,18 @@ +// logs.js +const util = require('../../utils/util.js') + +Page({ + data: { + logs: [] + }, + onLoad() { + this.setData({ + logs: (wx.getStorageSync('logs') || []).map(log => { + return { + date: util.formatTime(new Date(log)), + timeStamp: log + } + }) + }) + } +}) diff --git a/CommunicationRecords/pages/logs/logs.json b/CommunicationRecords/pages/logs/logs.json new file mode 100644 index 0000000..b55b5a2 --- /dev/null +++ b/CommunicationRecords/pages/logs/logs.json @@ -0,0 +1,4 @@ +{ + "usingComponents": { + } +} \ No newline at end of file diff --git a/CommunicationRecords/pages/logs/logs.wxml b/CommunicationRecords/pages/logs/logs.wxml new file mode 100644 index 0000000..85cf1bf --- /dev/null +++ b/CommunicationRecords/pages/logs/logs.wxml @@ -0,0 +1,6 @@ + + + + {{index + 1}}. {{log.date}} + + diff --git a/CommunicationRecords/pages/logs/logs.wxss b/CommunicationRecords/pages/logs/logs.wxss new file mode 100644 index 0000000..33f9d9e --- /dev/null +++ b/CommunicationRecords/pages/logs/logs.wxss @@ -0,0 +1,16 @@ +page { + height: 100vh; + display: flex; + flex-direction: column; +} +.scrollarea { + flex: 1; + overflow-y: hidden; +} +.log-item { + margin-top: 20rpx; + text-align: center; +} +.log-item:last-child { + padding-bottom: env(safe-area-inset-bottom); +} diff --git a/CommunicationRecords/project.config.json b/CommunicationRecords/project.config.json new file mode 100644 index 0000000..6ca16d2 --- /dev/null +++ b/CommunicationRecords/project.config.json @@ -0,0 +1,41 @@ +{ + "compileType": "miniprogram", + "libVersion": "trial", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "coverView": true, + "es6": true, + "postcss": true, + "minified": true, + "enhance": true, + "showShadowRootInWxmlPanel": true, + "packNpmRelationList": [], + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmManually": false, + "minifyWXSS": true, + "minifyWXML": true, + "localPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true, + "disableUseStrict": false, + "useCompilerPlugins": false + }, + "condition": {}, + "editorSetting": { + "tabIndent": "auto", + "tabSize": 2 + }, + "appid": "wx1322e65d8a20d902", + "simulatorPluginLibVersion": {} +} \ No newline at end of file diff --git a/CommunicationRecords/project.private.config.json b/CommunicationRecords/project.private.config.json new file mode 100644 index 0000000..fb3cd69 --- /dev/null +++ b/CommunicationRecords/project.private.config.json @@ -0,0 +1,23 @@ +{ + "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", + "projectname": "CommunicationRecords", + "setting": { + "compileHotReLoad": true, + "urlCheck": true, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "bigPackageSizeSupport": false, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true + }, + "libVersion": "3.11.0" +} \ No newline at end of file diff --git a/CommunicationRecords/sitemap.json b/CommunicationRecords/sitemap.json new file mode 100644 index 0000000..ca02add --- /dev/null +++ b/CommunicationRecords/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ + "action": "allow", + "page": "*" + }] +} \ No newline at end of file diff --git a/CommunicationRecords/utils/util.js b/CommunicationRecords/utils/util.js new file mode 100644 index 0000000..764bc2c --- /dev/null +++ b/CommunicationRecords/utils/util.js @@ -0,0 +1,19 @@ +const formatTime = date => { + const year = date.getFullYear() + const month = date.getMonth() + 1 + const day = date.getDate() + const hour = date.getHours() + const minute = date.getMinutes() + const second = date.getSeconds() + + return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}` +} + +const formatNumber = n => { + n = n.toString() + return n[1] ? n : `0${n}` +} + +module.exports = { + formatTime +} diff --git a/WxCheckApi/.config/dotnet-tools.json b/WxCheckApi/.config/dotnet-tools.json new file mode 100644 index 0000000..d4937e0 --- /dev/null +++ b/WxCheckApi/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.10", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/WxCheckApi/API文档.md b/WxCheckApi/API文档.md new file mode 100644 index 0000000..4a69d1a --- /dev/null +++ b/WxCheckApi/API文档.md @@ -0,0 +1,571 @@ +# 微信小程序:语音信息打卡 API 接口文档 + +本文档详细描述了WxCheck项目中Login控制器和Check控制器的所有API接口,包括接口功能、请求参数、响应格式以及调用示例。 + +## 基础信息 + +- **API基础路径**:`https://wx-xcx-check.blv-oa.com:4433/api/[controller]/[action]` +- **请求方式**:POST + +## 统一响应格式 + +所有接口返回以下统一格式的JSON响应: + +```json +{ + "success": true/false, + "message": "操作结果描述", + "data": {...}, // 可选,返回的具体数据 + "error": "错误信息" // 可选,错误时返回 +} +``` + +## 1. Login控制器接口 + +### 1.1 用户注册接口 + +#### 接口描述 +用户信息更新功能,根据传入的UserKey更新用户的UserName、WeChatName和PhoneNumber信息。 + +#### 接口路径 +`/api/Login/Register` + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| UserName | string | 否 | 用户名(可以为空) | +| UserKey | string | 是 | 用户唯一标识键 | +| WeChatName | string | 否 | 微信名称 | +| PhoneNumber | string | 否 | 电话号码 | +| AvatarUrl | string | 否 | 头像地址 | + +#### 请求示例 + +```json +{ + "UserName": "张三", + "UserKey": "openid_from_wechat", + "WeChatName": "张三的微信", + "PhoneNumber": "13800138000", + "AvatarUrl": "https://example.com/avatar.jpg" +} +``` + +#### 响应示例 + +**成功响应:** +```json +{ + "success": true, + "data": { + "Id": 1, + "UserName": "张三", + "UserKey": "openid_from_wechat", + "WeChatName": "张三的微信", + "PhoneNumber": "13800138000", + "AvatarUrl": "https://example.com/avatar.jpg", + "FirstLoginTime": "2023-10-31T10:00:00", + "IsDisabled": false, + "CreateTime": "2023-10-31T10:00:00", + "UpdateTime": "2023-10-31T15:30:00", + "Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } +} +``` + +**失败响应:** + +```json +{ + "success": false, + "message": "用户不存在" +} +``` + +```json +{ + "success": false, + "message": "更新用户信息失败", + "error": "数据库连接错误" +} +``` + +### 1.2 用户登录接口 + +#### 接口描述 +用户登录功能,将微信小程序code转换为OpenID并验证用户身份。如果用户不存在,则自动创建新用户记录。无论用户是否存在,都返回完整的用户信息和Token。 + +#### 接口路径 +`/api/Login/Login` + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| Code | string | 是 | 微信小程序登录凭证code,用于获取OpenID | + +#### 请求示例 + +```json +{ + "Code": "wx_login_code_here" +} +``` + +#### 响应示例 + +**成功响应(已存在用户):** +```json +{ + "success": true, + "data": { + "Id": 1, + "UserName": "张三", + "UserKey": "openid_from_wechat", + "WeChatName": "张三的微信", + "PhoneNumber": "13800138000", + "AvatarUrl": "https://example.com/avatar.jpg", + "FirstLoginTime": "2023-10-31T10:00:00", + "IsDisabled": false, + "CreateTime": "2023-10-31T10:00:00", + "UpdateTime": "2023-10-31T10:00:00", + "Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } +} +``` + +**成功响应(新用户自动注册):** +```json +{ + "success": true, + "data": { + "Id": 2, + "UserName": "", + "UserKey": "new_openid_from_wechat", + "WeChatName": "", + "PhoneNumber": "", + "AvatarUrl": "", + "FirstLoginTime": "2023-10-31T16:00:00", + "IsDisabled": false, + "CreateTime": "2023-10-31T16:00:00", + "UpdateTime": "2023-10-31T16:00:00", + "Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } +} +``` + +**失败响应:** +```json +{ + "success": false, + "message": "用户已被禁用" +} +``` + +```json +{ + "success": false, + "message": "获取微信OpenID失败", + "error": "微信API调用失败" +} +``` + +## 2. Check控制器接口 + +### 2.1 添加会话记录接口 + +#### 接口描述 +添加新的会话记录到系统中。 + +#### 接口路径 +`/api/Check/AddConversation` + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| UserKey | string | 是 | 用户唯一标识键 | +| ConversationContent | string | 是 | 会话内容 | +| SendMethod | string | 是 | 发送方式 | +| UserLocation | string | 否 | 用户定位信息 | +| MessageType | int | 否 | 1:公有消息,2:私有消息 | + +#### 请求示例 + +```json +{ + "UserKey": "user_123456", + "ConversationContent": "这是一条测试消息", + "SendMethod": "文本", + "UserLocation": "北京市海淀区", + "MessageType": 0 +} +``` + +#### 响应示例 + +**成功响应:** +```json +{ + "success": true, + "message": "收到!" +} +``` + +**失败响应:** +```json +{ + "success": false, + "message": "发送失败", + "error": "数据库操作错误" +} +``` + +### 2.2 查询会话记录接口 + +#### 接口描述 +根据用户唯一标识键查询该用户的所有会话记录。 + +#### 接口路径 +`https://wx-xcx-check.blv-oa.com/api/Check/GetConversations` + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| UserKey | string | 是 | 用户唯一标识键 | +| MessageType | int | 否 | 0:不判断消息类型(默认),1:只返回公有消息,2:只返回私有消息 | + +#### 请求示例 + +```json +{ + "UserKey": "user_123456", + "MessageType": 0 +} +``` + +#### 响应示例 + +**成功响应:** +```json +{ + "success": true, + "data": [ + { + "Id": 1, + "UserKey": "user_123456", + "ConversationContent": "这是一条测试消息", + "SendMethod": "文本", + "UserLocation": "北京市海淀区", + "RecordTime": "2023-10-31T10:05:00", + "RecordTimeUTCStamp": 1698732300000, + "IsDeleted": false, + "CreateTime": "2023-10-31T10:05:00", + "MessageType": 0 + }, + { + "Id": 2, + "UserKey": "user_123456", + "ConversationContent": "这是第二条测试消息", + "SendMethod": "图片", + "UserLocation": "北京市朝阳区", + "RecordTime": "2023-10-31T10:06:00", + "RecordTimeUTCStamp": 1698732360000, + "IsDeleted": false, + "CreateTime": "2023-10-31T10:06:00", + "MessageType": 1 + } + ] +} +``` + +**失败响应:** +```json +{ + "success": false, + "message": "查询失败", + "error": "数据库连接错误" +} +``` + +### 2.3 分页查询会话记录接口 + +#### 接口描述 +分页查询用户的会话记录,每页默认10条,按时间戳从**最新到最旧**排序,支持根据消息类型进行过滤。 + +#### 接口路径 +`https://wx-xcx-check.blv-oa.com:4433/api/Check/GetConversationsByPage` + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| UserKey | string | 是 | 用户唯一标识键 | +| Page | int | 否 | 页码,默认为1,小于1时自动设为1 | +| PageSize | int | 否 | 每页数量,默认为10,最大100,小于1时自动设为10 | +| MessageType | int | 否 | 0:不判断消息类型(默认),1:只返回公有消息,2:只返回私有消息 | + +#### 请求示例 + +```json +{ + "UserKey": "user_123456", + "Page": 1, + "PageSize": 10, + "MessageType": 0 +} +``` + +#### 响应示例 + +**成功响应:** +```json +{ + "success": true, + "data": { + "conversations": [ + { + "Id": 1, + "UserKey": "user_123456", + "ConversationContent": "这是一条测试消息", + "SendMethod": "文本", + "UserLocation": "北京市海淀区", + "RecordTime": "2023-10-31T10:00:00", + "RecordTimeUTCStamp": 1698727200000, + "IsDeleted": false, + "CreateTime": "2023-10-31T10:00:00", + "MessageType": 0 + }, + { + "Id": 2, + "UserKey": "user_123456", + "ConversationContent": "这是一条私有消息", + "SendMethod": "文本", + "UserLocation": "北京市朝阳区", + "RecordTime": "2023-10-31T09:55:00", + "RecordTimeUTCStamp": 1698724500000, + "IsDeleted": false, + "CreateTime": "2023-10-31T09:55:00", + "MessageType": 1 + } + ], + "totalCount": 25, + "page": 1, + "pageSize": 10, + "totalPages": 3 + } +} +``` + +**失败响应:** +```json +{ + "success": false, + "message": "查询失败", + "error": "数据库操作错误" +} +``` + +### 2.3 更新会话记录接口 + +#### 接口描述 +更新指定ID的会话记录,需要验证UserKey的权限。 + +#### 接口路径 +`/api/Check/UpdateConversation` + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| Id | long | 是 | 会话记录ID | +| UserKey | string | 是 | 用户唯一标识键(用于权限验证) | +| ConversationContent | string | 是 | 新的会话内容 | +| SendMethod | string | 是 | 新的发送方式 | +| UserLocation | string | 否 | 新的用户定位信息 | +| MessageType | int | 否 | 1:公有消息,2:私有消息 | + +#### 请求示例 + +```json +{ + "Id": 1, + "UserKey": "user_123456", + "ConversationContent": "更新后的会话内容", + "SendMethod": "文本", + "UserLocation": "北京市西城区", + "MessageType": 0 +} +``` + +#### 响应示例 + +**成功响应:** +```json +{ + "success": true, + "message": "会话更新成功" +} +``` + +**失败响应:** +```json +{ + "success": false, + "message": "会话记录不存在或无权限修改" +} +``` + +```json +{ + "success": false, + "message": "更新失败", + "error": "数据库操作错误" +} +``` + +### 2.4 删除会话记录接口 + +#### 接口描述 +软删除会话记录(将IsDeleted标记为1),需要验证UserKey的权限。 + +#### 接口路径 +`/api/Check/DeleteConversation` + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 描述 | +|--------|------|------|------| +| Id | long | 是 | 会话记录ID | +| UserKey | string | 是 | 用户唯一标识键(用于权限验证) | + +#### 请求示例 + +```json +{ + "Id": 1, + "UserKey": "user_123456" +} +``` + +#### 响应示例 + +**成功响应:** +```json +{ + "success": true, + "message": "会话删除成功" +} +``` + +**失败响应:** +```json +{ + "success": false, + "message": "会话记录不存在或已被删除" +} +``` + +```json +{ + "success": false, + "message": "删除失败", + "error": "数据库操作错误" +} +``` + +## 3. 接口调用说明 + +### 3.1 HTTP客户端调用示例(JavaScript) + +```javascript +// 封装API请求函数 +async function callApi(endpoint, data) { + try { + const response = await fetch(`http://your-api-domain/api${endpoint}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + return result; + } catch (error) { + console.error('API调用失败:', error); + throw error; + } +} + +// 用户信息更新示例 +async function updateUser() { + const result = await callApi('/Login/Register', { + UserName: '张三', + UserKey: 'openid_from_wechat', + WeChatName: '张三的微信', + PhoneNumber: '13800138000' + }); + + if (result.success) { + console.log('用户信息更新成功'); + } else { + console.log('用户信息更新失败:', result.message); + } +} + +// 用户登录示例 +async function loginUser() { + const result = await callApi('/Login/Login', { + Code: 'wx_login_code_here' + }); + + if (result.success) { + console.log('登录成功,用户信息:', result.data); + // 保存用户信息和Token到本地存储 + localStorage.setItem('userInfo', JSON.stringify(result.data)); + localStorage.setItem('token', result.data.Token); + } else { + console.log('登录失败:', result.message); + } +} + +// 添加会话示例 +async function addConversation() { + const result = await callApi('/Check/AddConversation', { + UserKey: 'user_123456', + ConversationContent: '测试会话内容', + SendMethod: '文本', + UserLocation: '北京', + MessageType: 0 + }); + + if (result.success) { + console.log('会话添加成功'); + } else { + console.log('会话添加失败:', result.message); + } +} +``` + +### 3.2 常见错误处理 + +1. **数据库连接错误**:检查数据库服务是否正常运行,连接字符串是否正确 +2. **权限验证失败**:确保提供的UserKey与操作资源匹配 +3. **记录不存在**:在更新或删除前确认记录ID和UserKey的正确性 +4. **网络错误**:检查API服务是否正常运行,网络连接是否稳定 + +## 4. 安全注意事项 + +1. **参数验证**:所有接口都应在前端进行基本的数据格式验证 +2. **UserKey保护**:UserKey作为用户身份标识,应妥善保护,避免泄露 +3. **错误信息处理**:生产环境中应避免返回详细的错误信息,防止信息泄露 +4. **请求频率限制**:建议在生产环境中对API接口实施请求频率限制,防止滥用 + +## 5. 接口维护信息 + +- **最后更新时间**:2023-11-01 +- **维护人员**:系统管理员 +- **版本号**:v1.1.0 \ No newline at end of file diff --git a/WxCheckApi/CSRedisCacheHelper.cs b/WxCheckApi/CSRedisCacheHelper.cs new file mode 100644 index 0000000..fef4abb --- /dev/null +++ b/WxCheckApi/CSRedisCacheHelper.cs @@ -0,0 +1,199 @@ +using CSRedis; +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using static CSRedis.CSRedisClient; + +namespace Common +{ + /// + /// Redis缓存辅助类 + /// + public class CSRedisCacheHelper + { + public static CSRedisClient? redis; + public static CSRedisClient? redis14; + public static CSRedisClient? redis15; + + private const string ip = "127.0.0.1"; + //private const string port = "6379"; + private const string port = "6800"; + static CSRedisCacheHelper() + { + var redisHostStr = string.Format("{0}:{1}", ip, port); + if (!string.IsNullOrEmpty(redisHostStr)) + { + redis = new CSRedisClient(redisHostStr + ",password=,defaultDatabase=0"); + redis15 = new CSRedisClient(redisHostStr + ",password=,defaultDatabase=15"); + var DingYueMsg = ("CellCorelDRAWUser", new Action(async (args) => + { + string body = args.Body; + })); + + CSRedisCacheHelper.redis.Subscribe(DingYueMsg); + } + } + /// + /// 添加缓存 + /// + /// + /// + /// + public static void Set(string key, T value, int ExpireTime) + { + redis?.Set(key, value, ExpireTime * 60); + } + + public static T Get(string key) + { + return redis.Get(key); + } + + public static void Forever(string key, T value) + { + redis.Set(key, value, -1); + } + public static void Del(string key) + { + redis.Del(key); + } + public static void ListPush(string key, T value) + { + redis.LPush(key, value); + } + + /// + /// 判断是否存在 + /// + /// + /// + + public static bool Contains(string key) + { + bool result = redis.Exists(key); + return result; + } + + public static string? XAdd(string key, params (string, string)[] fieldValues) + { + try + { + var result = redis.XAdd(key, fieldValues); + return result; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"XAdd error: {ex.Message}"); + return null; + } + } + + public static string? XReadGroup(string key, string group, string consumer, int count = 1, string id = null) + { + try + { + id = id ?? ">"; + var result = redis.XReadGroup(group, consumer, count, 0, (key, id)); + + if (result != null && result.Length > 0) + { + // 处理消息 + var messages = new List>(); + + foreach (var streamResult in result) + { + foreach (var entry in streamResult.data) + { + var message = new Dictionary + { + ["Id"] = entry.id, + ["Values"] = entry.items + }; + + messages.Add(message); + + // 确认消息已处理 + redis.XAck(key, group, entry.id); + } + } + + return System.Text.Json.JsonSerializer.Serialize(messages); + } + + return null; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"XReadGroup error: {ex.Message}"); + return null; + } + } + + public static (string key, (string id, string items)[] data)[] XReadGroup(string group, string consumer, long count, long block, params (string key, string id)[] streams) + { + try + { + var result = redis.XReadGroup(group, consumer, count, block, streams); + + if (result != null && result.Length > 0) + { + // 处理消息并确认已处理 + var processedResults = new List<(string key, (string id, string items)[] data)>(); + + foreach (var streamResult in result) + { + var messages = new List<(string id, string items)>(); + + foreach (var entry in streamResult.data) + { + // 确认消息已处理 + redis.XAck(streamResult.key, group, entry.id); + + // entry是一个元组 (string id, string[] items) + // 我们需要将string[] items转换为string items + var itemsArray = entry.items; + var itemsString = string.Join(",", itemsArray); + messages.Add((entry.id, itemsString)); + } + + processedResults.Add((streamResult.key, messages.ToArray())); + } + + return processedResults.ToArray(); + } + + return Array.Empty<(string key, (string id, string items)[] data)>(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"XReadGroup error: {ex.Message}"); + return Array.Empty<(string key, (string id, string items)[] data)>(); + } + } + + public static bool XGroupCreate(string key, string group, string id = "0") + { + try + { + redis.XGroupCreate(key, group, id, true); + return true; + } + catch + { + return false; + } + } + + /// + /// 发布消息 + /// + /// + /// + public static void Publish(string Topic, string Payload) + { + CSRedisCacheHelper.redis.PublishNoneMessageId(Topic, Payload); + } + } +} diff --git a/WxCheckApi/Controllers/CheckController.cs b/WxCheckApi/Controllers/CheckController.cs new file mode 100644 index 0000000..7014b5c --- /dev/null +++ b/WxCheckApi/Controllers/CheckController.cs @@ -0,0 +1,779 @@ +using Common; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using MySql.Data.MySqlClient; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using System.Web; + +namespace WxCheckApi.Controllers +{ + [Route("api/[controller]/[action]")] + [ApiController] + public class CheckController : ControllerBase + { + private readonly MySqlConnection _connection; + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + + public CheckController(MySqlConnection connection, IHttpClientFactory httpClientFactory, IConfiguration configuration) + { + _connection = connection; + _httpClient = httpClientFactory.CreateClient(); + _configuration = configuration; + } + + // 将经纬度转换为地址信息 + public async Task ConvertCoordinatesToAddress(string longitude,string latitude) + { + try + { + // 使用高德地图API进行逆地理编码 + string apiKey = _configuration["AmapApi:ApiKey"] ?? "4d5cb7818664ada68ae5f68783b8bd4c"; + string url = $"https://restapi.amap.com/v3/geocode/regeo?output=json&location={longitude},{latitude}&key={apiKey}&radius=1000&extensions=all"; + + var response = await _httpClient.GetStringAsync(url); + var jsonDoc = JsonDocument.Parse(response); + var root = jsonDoc.RootElement; + + + + if (root.GetProperty("status").GetString() == "1" && root.TryGetProperty("regeocode", out var regeocodeElement) && regeocodeElement.ValueKind != JsonValueKind.Null) + { + + if (regeocodeElement.TryGetProperty("formatted_address", out var formatted_address)) + { + return formatted_address.ToString(); + } + if (regeocodeElement.TryGetProperty("addressComponent", out var addressComponent)) + { + string province = addressComponent.TryGetProperty("province", out var provinceElement) && provinceElement.ValueKind == JsonValueKind.String ? provinceElement.GetString() : ""; + string city = addressComponent.TryGetProperty("city", out var cityElement) && cityElement.ValueKind == JsonValueKind.String ? cityElement.GetString() : ""; + string district = addressComponent.TryGetProperty("district", out var districtElement) && districtElement.ValueKind == JsonValueKind.String ? districtElement.GetString() : ""; + string township = addressComponent.TryGetProperty("township", out var townshipElement) && townshipElement.ValueKind == JsonValueKind.String ? townshipElement.GetString() : ""; + + // 获取街道和门牌号信息 + string street = ""; + string streetNumber = ""; + double distance = 0; + + // 方法1:从addressComponent获取街道信息 + if (addressComponent.TryGetProperty("streetNumber", out var streetNumberElement) && streetNumberElement.ValueKind == JsonValueKind.Object) + { + street = streetNumberElement.TryGetProperty("street", out var streetElement) && streetElement.ValueKind == JsonValueKind.String ? streetElement.GetString() : ""; + streetNumber = streetNumberElement.TryGetProperty("number", out var numberElement) && numberElement.ValueKind == JsonValueKind.String ? numberElement.GetString() : ""; + + // 获取距离信息 + if (streetNumberElement.TryGetProperty("distance", out var distanceElement) && distanceElement.ValueKind == JsonValueKind.String) + { + double.TryParse(distanceElement.GetString(), out distance); + } + } + + // 方法2:如果方法1没有获取到街道信息,尝试从aoi信息中获取 + if (string.IsNullOrEmpty(street) && regeocodeElement.TryGetProperty("aois", out var aoisElement) && aoisElement.ValueKind == JsonValueKind.Array) + { + var aoisArray = aoisElement.EnumerateArray(); + foreach (var aoi in aoisArray) + { + if (aoi.TryGetProperty("name", out var aoiNameElement) && aoiNameElement.ValueKind == JsonValueKind.String) + { + street = aoiNameElement.GetString(); + break; // 取第一个AOI作为街道信息 + } + } + } + + // 方法3:如果前两种方法都没有获取到街道信息,尝试从pois信息中获取 + if (string.IsNullOrEmpty(street) && regeocodeElement.TryGetProperty("pois", out var poisElement) && poisElement.ValueKind == JsonValueKind.Array) + { + var poisArray = poisElement.EnumerateArray(); + foreach (var poi in poisArray) + { + if (poi.TryGetProperty("name", out var poiNameElement) && poiNameElement.ValueKind == JsonValueKind.String) + { + street = poiNameElement.GetString(); + break; // 取第一个POI作为街道信息 + } + } + } + + // 方法4:如果以上方法都没有获取到街道信息,尝试从formatted_address中解析 + if (string.IsNullOrEmpty(street) && regeocodeElement.TryGetProperty("formatted_address", out var formattedAddressElement) && formattedAddressElement.ValueKind == JsonValueKind.String) + { + string formattedAddress = formattedAddressElement.GetString(); + // 尝试从格式化地址中提取街道信息 + // 格式化地址通常格式为:省 市 区 街道 具体地址 + + // 使用字符串数组作为分隔符 + string[] separators = { " ", "省", "市", "区", "县", "镇", "街道", "路", "巷", "号" }; + var addressParts = formattedAddress.Split(separators, StringSplitOptions.RemoveEmptyEntries); + + // 查找可能包含街道信息的部分 + for (int i = 0; i < addressParts.Length; i++) + { + var part = addressParts[i]; + // 如果部分包含"路"、"街"、"巷"等关键词,可能是街道信息 + if (part.Contains("路") || part.Contains("街") || part.Contains("巷") || part.Contains("道")) + { + street = part; + // 如果下一个部分存在且不是区县名称,可能是门牌号 + if (i + 1 < addressParts.Length && + !addressParts[i + 1].Contains("区") && + !addressParts[i + 1].Contains("县")) + { + streetNumber = addressParts[i + 1]; + } + break; + } + } + } + + // 方法5:如果仍然没有获取到街道信息,尝试从nearestRoad信息中获取 + if (string.IsNullOrEmpty(street) && regeocodeElement.TryGetProperty("streetNumber", out var nearestStreetElement) && + nearestStreetElement.ValueKind == JsonValueKind.Object) + { + if (nearestStreetElement.TryGetProperty("street", out var nearestStreetNameElement) && nearestStreetNameElement.ValueKind == JsonValueKind.String) + { + street = nearestStreetNameElement.GetString(); + } + } + + // 构建详细地址字符串 + string address = ""; + if (!string.IsNullOrEmpty(province)) + { + address += province; + } + if (!string.IsNullOrEmpty(city) && city != province) + { + address += " " + city; + } + if (!string.IsNullOrEmpty(district)) + { + address += " " + district; + } + if (!string.IsNullOrEmpty(township)) + { + address += " " + township; + } + if (!string.IsNullOrEmpty(street)) + { + address += " " + street; + } + if (!string.IsNullOrEmpty(streetNumber)) + { + address += " " + streetNumber; + } + + // 如果有距离信息,添加到地址后面 + if (distance > 0) + { + address += $" {distance:F1}米"; + } + if (string.IsNullOrEmpty(address)) + { + return "未获取到位置信息(高德返回值为空) " + latitude + "," + longitude; + } + return address.Trim(); + } + } + + return latitude + "," + longitude; ; // 如果API调用失败,返回原始值 + } + catch (Exception) + { + return latitude + "," + longitude; ; // 如果发生异常,返回原始值 + } + } + + [HttpPost] + public async Task CheckAddress([FromBody] CheckAddressRequest request) + { + try + { + if (_connection.State != ConnectionState.Open) + { + await _connection.OpenAsync(); + } + + // 从数据库查询经纬度信息 + string latitude = ""; + string longitude = ""; + + using (MySqlCommand cmd = new MySqlCommand("SELECT Latitude, Longitude FROM xcx_conversation WHERE Id = @Id AND IsDeleted = 0", _connection)) + { + cmd.Parameters.AddWithValue("@Id", request.Id); + + using (var reader = await cmd.ExecuteReaderAsync()) + { + if (await reader.ReadAsync()) + { + latitude = reader.IsDBNull(0) ? "" : reader.GetString(0); + longitude = reader.IsDBNull(1) ? "" : reader.GetString(1); + } + else + { + return NotFound(new { success = false, message = "记录不存在或已被删除" }); + } + } + } + + // 转换经纬度为地址 + var address = await ConvertCoordinatesToAddress(longitude, latitude); + + // 更新数据库中的UserLocation字段 + using (MySqlCommand cmd = new MySqlCommand("UPDATE xcx_conversation SET UserLocation = @UserLocation WHERE Id = @Id AND IsDeleted = 0", _connection)) + { + cmd.Parameters.AddWithValue("@Id", request.Id); + cmd.Parameters.AddWithValue("@UserLocation", address); + + int rowsAffected = await cmd.ExecuteNonQueryAsync(); + + if (rowsAffected == 0) + { + return NotFound(new { success = false, message = "记录不存在或已被删除" }); + } + } + + return Ok(new { success = true, message = "地址更新成功", address = address }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "更新失败", error = ex.Message }); + } + finally + { + if (_connection.State == ConnectionState.Open) + { + await _connection.CloseAsync(); + } + } + } + // 添加会话记录 + [HttpPost] + public async Task AddConversation([FromBody] ConversationRequest request) + { + try + { + if (_connection.State != ConnectionState.Open) + { + await _connection.OpenAsync(); + } + + // 解析经纬度并转换为地址 + string address = ""; + string latitude = ""; + string longitude = ""; + + // 否则尝试从UserLocation字段解析 + if (!string.IsNullOrEmpty(request.UserLocation)) + { + string[] parts = request.UserLocation.Split(','); + if (parts.Length == 2) + { + if (double.TryParse(parts[0], out double lat) && double.TryParse(parts[1], out double lng)) + { + longitude = lng.ToString(); + latitude = lat.ToString(); + address = "";// await ConvertCoordinatesToAddress(latitude, longitude); + } + } + } + + long conversationId = 0; + using (MySqlCommand cmd = new MySqlCommand("INSERT INTO xcx_conversation (UserKey, ConversationContent, SendMethod, UserLocation, Latitude, Longitude, RecordTime, RecordTimeUTCStamp, IsDeleted, CreateTime, MessageType) VALUES (@UserKey, @ConversationContent, @SendMethod, @UserLocation, @Latitude, @Longitude, @RecordTime, @RecordTimeUTCStamp, @IsDeleted, NOW(), @MessageType); SELECT LAST_INSERT_ID();", _connection)) + { + cmd.Parameters.AddWithValue("@UserKey", request.UserKey); + cmd.Parameters.AddWithValue("@MessageType", request.MessageType); + cmd.Parameters.AddWithValue("@ConversationContent", request.ConversationContent); + cmd.Parameters.AddWithValue("@SendMethod", request.SendMethod); + cmd.Parameters.AddWithValue("@UserLocation", address); + cmd.Parameters.AddWithValue("@Latitude", latitude); + cmd.Parameters.AddWithValue("@Longitude", longitude); + cmd.Parameters.AddWithValue("@RecordTime", DateTime.Now); + cmd.Parameters.AddWithValue("@RecordTimeUTCStamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + cmd.Parameters.AddWithValue("@IsDeleted", 0); + + object result = await cmd.ExecuteScalarAsync(); + conversationId = Convert.ToInt64(result); + } + + // 查询刚插入的记录,并左连接用户表 + if (conversationId > 0) + { + string query = @"SELECT convs.Id, convs.UserKey, convs.ConversationContent, convs.SendMethod, + convs.UserLocation, convs.Latitude, convs.Longitude, convs.RecordTime, + convs.RecordTimeUTCStamp, convs.IsDeleted, convs.CreateTime, convs.MessageType, + users.UserName, users.WeChatName, users.PhoneNumber, users.AvatarUrl + FROM xcx_conversation AS convs + LEFT JOIN xcx_users AS users ON convs.UserKey = users.UserKey + WHERE convs.Id = @Id"; + + using (MySqlCommand cmd = new MySqlCommand(query, _connection)) + { + cmd.Parameters.AddWithValue("@Id", conversationId); + + using (var reader = await cmd.ExecuteReaderAsync()) + { + if (await reader.ReadAsync()) + { + // 构建要发送到Redis的数据 + var messageData = new Dictionary + { + ["Id"] = reader.GetInt64(0).ToString(), + ["UserKey"] = reader.GetString(1), + ["ConversationContent"] = reader.GetString(2), + ["SendMethod"] = reader.GetString(3), + ["UserLocation"] = reader.IsDBNull(4) ? "" : reader.GetString(4), + ["Latitude"] = reader.IsDBNull(5) ? "" : reader.GetString(5), + ["Longitude"] = reader.IsDBNull(6) ? "" : reader.GetString(6), + ["RecordTime"] = reader.GetDateTime(7).ToString("yyyy-MM-dd HH:mm:ss"), + ["RecordTimeUTCStamp"] = reader.GetInt64(8).ToString(), + ["IsDeleted"] = reader.GetBoolean(9).ToString(), + ["CreateTime"] = reader.GetDateTime(10).ToString("yyyy-MM-dd HH:mm:ss"), + ["MessageType"] = reader.GetInt32(11).ToString(), + ["UserName"] = reader.IsDBNull(12) ? "" : reader.GetString(12), + ["WeChatName"] = reader.IsDBNull(13) ? "" : reader.GetString(13), + ["PhoneNumber"] = reader.IsDBNull(14) ? "" : reader.GetString(14), + ["AvatarUrl"] = reader.IsDBNull(15) ? "" : reader.GetString(15) + }; + + // 发送到Redis Stream + try + { + // 确保Stream和Group存在 + CSRedisCacheHelper.XGroupCreate("xcx_msg", "xcx_group", "0"); + + // 将Dictionary转换为params (string, string)[]格式 + var fieldValues = messageData.SelectMany(kvp => new (string, string)[] { (kvp.Key, kvp.Value) }).ToArray(); + + // 添加消息到Stream + string messageId = CSRedisCacheHelper.XAdd("xcx_msg", fieldValues); + + // 记录日志(可选) + System.Diagnostics.Debug.WriteLine($"消息已发送到Redis Stream: {messageId}"); + } + catch (Exception ex) + { + // 记录错误但不影响主流程 + System.Diagnostics.Debug.WriteLine($"发送到Redis Stream失败: {ex.Message}"); + } + } + } + } + } + + return Ok(new { success = true, message = "收到!", conversationId = conversationId }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "发送失败", error = ex.Message }); + } + finally + { + if (_connection.State == ConnectionState.Open) + { + await _connection.CloseAsync(); + } + } + } + + // 根据UserKey查询会话记录 + [HttpPost] + public async Task GetConversations([FromBody] UserKeyRequest request) + { + try + { + if (_connection.State != ConnectionState.Open) + { + await _connection.OpenAsync(); + } + + List conversations = new List(); + + // 构建查询SQL,根据MessageType参数决定是否添加过滤条件 + string query = "SELECT Id, UserKey, ConversationContent, SendMethod, UserLocation, Latitude, Longitude, RecordTime, RecordTimeUTCStamp, IsDeleted, CreateTime, MessageType FROM xcx_conversation WHERE UserKey = @UserKey AND IsDeleted = 0"; + if (request.MessageType == 1) + { + query += " AND MessageType = @MessageType"; + } + query += " ORDER BY RecordTimeUTCStamp DESC"; + + using (MySqlCommand cmd = new MySqlCommand(query, _connection)) + { + cmd.Parameters.AddWithValue("@UserKey", request.UserKey); + + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + conversations.Add(new ConversationResponse + { + Id = reader.GetInt64(0), + UserKey = reader.GetString(1), + ConversationContent = reader.GetString(2), + SendMethod = reader.GetString(3), + UserLocation = reader.IsDBNull(4) ? "" : reader.GetString(4), + Latitude = reader.IsDBNull(5) ? "" : reader.GetString(5), + Longitude = reader.IsDBNull(6) ? "" : reader.GetString(6), + RecordTime = reader.GetDateTime(7), + RecordTimeUTCStamp = reader.GetInt64(8), + IsDeleted = reader.GetBoolean(9), + CreateTime = reader.GetDateTime(10), + MessageType = reader.GetInt32(11) + }); + } + } + } + + return Ok(new { success = true, data = conversations.OrderBy(z => z.RecordTimeUTCStamp) }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "查询失败", error = ex.Message }); + } + finally + { + if (_connection.State == ConnectionState.Open) + { + await _connection.CloseAsync(); + } + } + } + + // 更新会话记录 + [HttpPost] + public async Task UpdateConversation([FromBody] UpdateConversationRequest request) + { + try + { + if (_connection.State != ConnectionState.Open) + { + await _connection.OpenAsync(); + } + + using (MySqlCommand cmd = new MySqlCommand("UPDATE xcx_conversation SET ConversationContent = @ConversationContent, SendMethod = @SendMethod, UserLocation = @UserLocation, MessageType = @MessageType WHERE Id = @Id AND UserKey = @UserKey", _connection)) + { + cmd.Parameters.AddWithValue("@Id", request.Id); + cmd.Parameters.AddWithValue("@UserKey", request.UserKey); + cmd.Parameters.AddWithValue("@ConversationContent", request.ConversationContent); + cmd.Parameters.AddWithValue("@SendMethod", request.SendMethod); + cmd.Parameters.AddWithValue("@UserLocation", request.UserLocation ?? ""); + cmd.Parameters.AddWithValue("@MessageType", request.MessageType); + + int rowsAffected = await cmd.ExecuteNonQueryAsync(); + + if (rowsAffected == 0) + { + return NotFound(new { success = false, message = "记录不存在或无权限修改" }); + } + } + + return Ok(new { success = true, message = "更新成功" }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "更新失败", error = ex.Message }); + } + finally + { + if (_connection.State == ConnectionState.Open) + { + await _connection.CloseAsync(); + } + } + } + + // 软删除会话记录 + [HttpPost] + public async Task DeleteConversation([FromBody] DeleteConversationRequest request) + { + try + { + if (_connection.State != ConnectionState.Open) + { + await _connection.OpenAsync(); + } + + using (MySqlCommand cmd = new MySqlCommand("UPDATE xcx_conversation SET IsDeleted = 1 WHERE Id = @Id AND UserKey = @UserKey AND IsDeleted = 0", _connection)) + { + cmd.Parameters.AddWithValue("@Id", request.Id); + cmd.Parameters.AddWithValue("@UserKey", request.UserKey); + + int rowsAffected = await cmd.ExecuteNonQueryAsync(); + + if (rowsAffected == 0) + { + return NotFound(new { success = false, message = "记录不存在或已被删除" }); + } + } + + return Ok(new { success = true, message = "删除成功" }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "删除失败", error = ex.Message }); + } + finally + { + if (_connection.State == ConnectionState.Open) + { + await _connection.CloseAsync(); + } + } + } + + // 分页查询会话记录 + [HttpPost] + public async Task GetConversationsByPage([FromBody] PaginationRequest request) + { + try + { + if (_connection.State != ConnectionState.Open) + { + await _connection.OpenAsync(); + } + + // 验证并设置默认值 + if (request.Page < 1) request.Page = 1; + if (request.PageSize < 1 || request.PageSize > 100) request.PageSize = 10; + + int offset = (request.Page - 1) * request.PageSize; + + List conversations = new List(); + + // 构建分页查询SQL,根据MessageType参数决定是否添加过滤条件 + string query = @"SELECT Id, UserKey, ConversationContent, SendMethod, UserLocation, Latitude, Longitude, RecordTime, RecordTimeUTCStamp, IsDeleted, CreateTime, MessageType + FROM xcx_conversation + WHERE UserKey = @UserKey AND IsDeleted = 0"; + if (request.MessageType == 1) + { + query += " AND MessageType = @MessageType"; + } + query += " ORDER BY RecordTimeUTCStamp DESC LIMIT @Offset, @Limit"; + + using (MySqlCommand cmd = new MySqlCommand(query, _connection)) + { + cmd.Parameters.AddWithValue("@UserKey", request.UserKey); + if (request.MessageType == 1) + { + cmd.Parameters.AddWithValue("@MessageType", request.MessageType); + } + cmd.Parameters.AddWithValue("@Offset", offset); + cmd.Parameters.AddWithValue("@Limit", request.PageSize); + + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + conversations.Add(new ConversationResponse + { + Id = reader.GetInt64(0), + UserKey = reader.GetString(1), + ConversationContent = reader.GetString(2), + SendMethod = reader.GetString(3), + UserLocation = reader.IsDBNull(4) ? "" : reader.GetString(4), + Latitude = reader.IsDBNull(5) ? "" : reader.GetString(5), + Longitude = reader.IsDBNull(6) ? "" : reader.GetString(6), + RecordTime = reader.GetDateTime(7), + RecordTimeUTCStamp = reader.GetInt64(8), + IsDeleted = reader.GetBoolean(9), + CreateTime = reader.GetDateTime(10), + MessageType = reader.GetInt32(11) + }); + } + } + } + + // 查询总数,根据MessageType参数决定是否添加过滤条件 + int totalCount = 0; + string countQuery = "SELECT COUNT(*) FROM xcx_conversation WHERE UserKey = @UserKey AND IsDeleted = 0"; + if (request.MessageType == 1) + { + countQuery += " AND MessageType = @MessageType"; + } + + using (MySqlCommand countCmd = new MySqlCommand(countQuery, _connection)) + { + countCmd.Parameters.AddWithValue("@UserKey", request.UserKey); + if (request.MessageType == 1) + { + countCmd.Parameters.AddWithValue("@MessageType", request.MessageType); + } + totalCount = Convert.ToInt32(await countCmd.ExecuteScalarAsync()); + } + + int totalPages = (int)Math.Ceiling((double)totalCount / request.PageSize); + + return Ok(new { + success = true, + data = new { + conversations = conversations.OrderBy(z => z.RecordTimeUTCStamp), + totalCount = totalCount, + page = request.Page, + pageSize = request.PageSize, + totalPages = totalPages + } + }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "查询失败", error = ex.Message }); + } + finally + { + if (_connection.State == ConnectionState.Open) + { + await _connection.CloseAsync(); + } + } + } + + // 从Redis Stream读取消息 + [HttpPost] + public async Task ReadMessageFromRedis([FromBody] RedisMessageRequest request) + { + try + { + // 确保Stream和Group存在 + CSRedisCacheHelper.XGroupCreate("xcx_msg", "xcx_group", "0"); + + // 从Redis Stream读取消息 + string groupName = request.GroupName ?? "xcx_group"; + string consumerName = request.ConsumerName ?? "consumer_" + DateTime.Now.Ticks; + + string messages = CSRedisCacheHelper.XReadGroup("xcx_msg", groupName, consumerName, request.Count ?? 1); + + if (string.IsNullOrEmpty(messages)) + { + return Ok(new { success = true, message = "没有新消息", data = new List() }); + } + + // 解析消息 + var messageList = new List(); + + try + { + var messageEntries = System.Text.Json.JsonSerializer.Deserialize>>(messages); + + if (messageEntries != null) + { + foreach (var entry in messageEntries) + { + if (entry.TryGetValue("Id", out var id) && + entry.TryGetValue("Values", out var values)) + { + var messageData = new Dictionary(); + + // 如果Values是JsonElement,需要进一步解析 + if (values is JsonElement valuesElement && valuesElement.ValueKind == JsonValueKind.Object) + { + foreach (var property in valuesElement.EnumerateObject()) + { + messageData[property.Name] = property.Value.GetString() ?? ""; + } + } + else if (values is Dictionary valuesDict) + { + messageData = valuesDict; + } + + messageData["MessageId"] = id.ToString(); + messageList.Add(messageData); + } + } + } + } + catch (Exception parseEx) + { + // 如果解析失败,尝试直接返回原始消息 + System.Diagnostics.Debug.WriteLine($"解析消息失败: {parseEx.Message}"); + messageList.Add(new { RawMessage = messages }); + } + + return Ok(new { success = true, message = "成功读取消息", data = messageList }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "读取消息失败", error = ex.Message }); + } + } + } + + // 请求和响应模型 + public class ConversationRequest + { + public string UserKey { get; set; } + public string ConversationContent { get; set; } + public string SendMethod { get; set; } + public string UserLocation { get; set; } + public double Latitude { get; set; } + public double Longitude { get; set; } + public int MessageType { get; set; } = 1; // 1:公有,2:私有 + } + + public class UserKeyRequest + { + public string UserKey { get; set; } + public int MessageType { get; set; } = 0; // 0:不判断消息类型,1:公有,2:私有 + } + + public class PaginationRequest + { + public string UserKey { get; set; } + public int Page { get; set; } = 1; // 默认第一页 + public int PageSize { get; set; } = 10; // 默认每页10条 + public int MessageType { get; set; } = 0; // 0:不判断消息类型,1:公有,2:私有 + } + + public class UpdateConversationRequest + { + public long Id { get; set; } + public string UserKey { get; set; } + public string ConversationContent { get; set; } + public string SendMethod { get; set; } + public string UserLocation { get; set; } + public int MessageType { get; set; } = 0; + } + + public class DeleteConversationRequest + { + public long Id { get; set; } + public string UserKey { get; set; } + } + public class CheckAddressRequest + { + public long Id { get; set; } + } + + public class ConversationResponse + { + public long Id { get; set; } + public string UserKey { get; set; } + public string ConversationContent { get; set; } + public string SendMethod { get; set; } + public string UserLocation { get; set; } + public string Latitude { get; set; } + public string Longitude { get; set; } + public DateTime RecordTime { get; set; } + public long RecordTimeUTCStamp { get; set; } + public bool IsDeleted { get; set; } + public DateTime CreateTime { get; set; } + public int MessageType { get; set; } // 1:公有消息,2:私有消息 + } + + public class RedisMessageRequest + { + public string GroupName { get; set; } = "xcx_group"; + public string ConsumerName { get; set; } + public int? Count { get; set; } = 1; + } +} diff --git a/WxCheckApi/Controllers/LoginController.cs b/WxCheckApi/Controllers/LoginController.cs new file mode 100644 index 0000000..b5c8b90 --- /dev/null +++ b/WxCheckApi/Controllers/LoginController.cs @@ -0,0 +1,324 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using MySql.Data.MySqlClient; +using System; +using System.Data; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Net.Http; +using System.Text.Json; + +namespace WxCheckApi.Controllers +{ + [Route("api/[controller]/[action]")] + [ApiController] + public class LoginController : ControllerBase + { + private readonly MySqlConnection _connection; + private readonly IHttpClientFactory _httpClientFactory; + public IConfiguration? configuration { get; set; } + + public LoginController(MySqlConnection connection, IHttpClientFactory httpClientFactory, IConfiguration? configuration) + { + _connection = connection; + _httpClientFactory = httpClientFactory; + this.configuration = configuration; + } + + // 获取微信小程序OpenID + private async Task GetWxOpenIdAsync(string code) + { + try + { + var appId = configuration["WeChat:AppId"]; + var appSecret = configuration["WeChat:AppSecret"]; + + if (string.IsNullOrEmpty(appId) || string.IsNullOrEmpty(appSecret)) + { + throw new Exception("微信小程序配置缺失"); + } + + var httpClient = _httpClientFactory.CreateClient(); + var url = $"https://api.weixin.qq.com/sns/jscode2session?appid={appId}&secret={appSecret}&js_code={code}&grant_type=authorization_code"; + + var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var responseContent = await response.Content.ReadAsStringAsync(); + var jsonDocument = JsonDocument.Parse(responseContent); + + if (jsonDocument.RootElement.TryGetProperty("openid", out var openidElement)) + { + return openidElement.GetString(); + } + else + { + // 如果有错误信息,抛出异常 + if (jsonDocument.RootElement.TryGetProperty("errcode", out var errcodeElement) && + jsonDocument.RootElement.TryGetProperty("errmsg", out var errmsgElement)) + { + throw new Exception($"获取OpenID失败: {errcodeElement.GetInt32()} - {errmsgElement.GetString()}"); + } + throw new Exception("获取OpenID失败: 响应中未包含openid"); + } + } + catch (Exception ex) + { + throw new Exception($"获取微信OpenID时发生错误: {ex.Message}"); + } + } + + + private string GetToken(string entity) + { + string TokenString; + var claims = new Claim[] + { + new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()), + new Claim(ClaimTypes.Name, entity) + }; + + var secretByte = Encoding.UTF8.GetBytes(configuration["JwT:SecretKey"]); + var signingKey = new SymmetricSecurityKey(secretByte); + var a = SecurityAlgorithms.HmacSha256; + + var signingCredentials = new SigningCredentials(signingKey, a); + + var token = new JwtSecurityToken( + issuer: configuration["JwT:Issuer"], + audience: configuration["JwT:Audience"],//接收 + claims: claims,//存放的用户信息 + notBefore: DateTime.UtcNow,//发布时间 + expires: DateTime.UtcNow.AddMonths(12), + signingCredentials: signingCredentials + //有效期设置为1天signingCredentials //数字名 + ); + TokenString = new JwtSecurityTokenHandler().WriteToken(token); + return TokenString; + } + + // 用户注册接口 + [HttpPost] + public async Task Register([FromBody] RegisterRequest request) + { + try + { + if (_connection.State != ConnectionState.Open) + { + await _connection.OpenAsync(); + } + + // 检查用户是否存在 + UserResponse user = null; + using (MySqlCommand checkCmd = new MySqlCommand("SELECT Id, UserName, UserKey, WeChatName, PhoneNumber, AvatarUrl, FirstLoginTime, IsDisabled, CreateTime, UpdateTime FROM xcx_users WHERE UserKey = @UserKey", _connection)) + { + checkCmd.Parameters.AddWithValue("@UserKey", request.UserKey); + using (var reader = await checkCmd.ExecuteReaderAsync()) + { + if (await reader.ReadAsync()) + { + user = new UserResponse + { + Id = reader.GetInt64(0), + UserName = reader.IsDBNull(1) ? "" : reader.GetString(1), + UserKey = reader.GetString(2), + WeChatName = reader.IsDBNull(3) ? "" : reader.GetString(3), + PhoneNumber = reader.IsDBNull(4) ? "" : reader.GetString(4), + AvatarUrl = reader.IsDBNull(5) ? "" : reader.GetString(5), + FirstLoginTime = reader.GetDateTime(6), + IsDisabled = reader.GetBoolean(7), + CreateTime = reader.GetDateTime(8), + UpdateTime = reader.GetDateTime(9) + }; + } + } + } + + if (user == null) + { + return NotFound(new { success = false, message = "用户不存在" }); + } + + // 更新用户信息 + using (MySqlCommand cmd = new MySqlCommand("UPDATE xcx_users SET UserName = @UserName, WeChatName = @WeChatName, PhoneNumber = @PhoneNumber, AvatarUrl = @AvatarUrl, UpdateTime = NOW() WHERE UserKey = @UserKey", _connection)) + { + cmd.Parameters.AddWithValue("@UserName", request.UserName ?? (object)DBNull.Value); + cmd.Parameters.AddWithValue("@WeChatName", request.WeChatName ?? ""); + cmd.Parameters.AddWithValue("@PhoneNumber", request.PhoneNumber ?? ""); + cmd.Parameters.AddWithValue("@AvatarUrl", request.AvatarUrl ?? (object)DBNull.Value); + cmd.Parameters.AddWithValue("@UserKey", request.UserKey); + + await cmd.ExecuteNonQueryAsync(); + } + + // 获取更新后的用户信息 + UserResponse updatedUser = null; + using (MySqlCommand cmd = new MySqlCommand("SELECT Id, UserName, UserKey, WeChatName, PhoneNumber, AvatarUrl, FirstLoginTime, IsDisabled, CreateTime, UpdateTime FROM xcx_users WHERE UserKey = @UserKey", _connection)) + { + cmd.Parameters.AddWithValue("@UserKey", request.UserKey); + using (var reader = await cmd.ExecuteReaderAsync()) + { + if (await reader.ReadAsync()) + { + updatedUser = new UserResponse + { + Id = reader.GetInt64(0), + UserName = reader.IsDBNull(1) ? "" : reader.GetString(1), + UserKey = reader.GetString(2), + WeChatName = reader.IsDBNull(3) ? "" : reader.GetString(3), + PhoneNumber = reader.IsDBNull(4) ? "" : reader.GetString(4), + AvatarUrl = reader.IsDBNull(5) ? "" : reader.GetString(5), + FirstLoginTime = reader.GetDateTime(6), + IsDisabled = reader.GetBoolean(7), + CreateTime = reader.GetDateTime(8), + UpdateTime = reader.GetDateTime(9), + Token = GetToken(request.UserKey) + }; + } + } + } + + return Ok(new { success = true, data = updatedUser }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "更新用户信息失败", error = ex.Message }); + } + finally + { + if (_connection.State == ConnectionState.Open) + { + await _connection.CloseAsync(); + } + } + } + + // 用户登录接口 + [HttpPost] + public async Task Login([FromBody] LoginRequest request) + { + try + { + string openId; + try + { + openId = await GetWxOpenIdAsync(request.Code); + } + catch (Exception ex) + { + return BadRequest(new { success = false, message = "获取微信OpenID失败", error = ex.Message }); + } + + if (_connection.State != ConnectionState.Open) + { + await _connection.OpenAsync(); + } + + UserResponse user = null; + + // 检查用户是否存在 + using (MySqlCommand checkCmd = new MySqlCommand("SELECT COUNT(1) FROM xcx_users WHERE UserKey = @UserKey", _connection)) + { + checkCmd.Parameters.AddWithValue("@UserKey", openId); + int count = Convert.ToInt32(await checkCmd.ExecuteScalarAsync()); + + // 如果用户不存在,则注册新用户 + if (count == 0) + { + using (MySqlCommand insertCmd = new MySqlCommand("INSERT INTO xcx_users (UserKey, FirstLoginTime, IsDisabled, CreateTime, UpdateTime) VALUES (@UserKey, @FirstLoginTime, @IsDisabled, NOW(), NOW())", _connection)) + { + insertCmd.Parameters.AddWithValue("@UserKey", openId); + insertCmd.Parameters.AddWithValue("@FirstLoginTime", DateTime.Now); + insertCmd.Parameters.AddWithValue("@IsDisabled", 0); // 默认启用 + + await insertCmd.ExecuteNonQueryAsync(); + } + } + } + + // 获取用户信息 + using (MySqlCommand cmd = new MySqlCommand("SELECT Id, UserName, UserKey, WeChatName, PhoneNumber, AvatarUrl, FirstLoginTime, IsDisabled, CreateTime, UpdateTime FROM xcx_users WHERE UserKey = @UserKey", _connection)) + { + cmd.Parameters.AddWithValue("@UserKey", openId); + + using (var reader = await cmd.ExecuteReaderAsync()) + { + if (await reader.ReadAsync()) + { + user = new UserResponse + { + Id = reader.GetInt64(0), + UserName = reader.IsDBNull(1) ? "" : reader.GetString(1), + UserKey = reader.GetString(2), + WeChatName = reader.IsDBNull(3) ? "" : reader.GetString(3), + PhoneNumber = reader.IsDBNull(4) ? "" : reader.GetString(4), + AvatarUrl = reader.IsDBNull(5) ? "" : reader.GetString(5), + FirstLoginTime = reader.GetDateTime(6), + IsDisabled = reader.GetBoolean(7), + CreateTime = reader.GetDateTime(8), + UpdateTime = reader.GetDateTime(9), + Token = GetToken(openId) + }; + } + } + } + + if (user == null) + { + return NotFound(new { success = false, message = "用户不存在" }); + } + + if (user.IsDisabled) + { + return Ok(new { success = false, message = "用户已被禁用" }); + } + + return Ok(new { success = true, data = user }); + } + catch (Exception ex) + { + return StatusCode(500, new { success = false, message = "登录失败", error = ex.Message }); + } + finally + { + if (_connection.State == ConnectionState.Open) + { + await _connection.CloseAsync(); + } + } + } + } + + // 请求和响应模型 + public class RegisterRequest + { + public string UserName { get; set; } + public string UserKey { get; set; } // 改为直接传入UserKey + public string WeChatName { get; set; } + public string PhoneNumber { get; set; } + public string AvatarUrl { get; set; } + } + + public class LoginRequest + { + public string Code { get; set; } + } + + public class UserResponse + { + public long Id { get; set; } + public string UserName { get; set; } + public string UserKey { get; set; } + public string WeChatName { get; set; } + public string PhoneNumber { get; set; } + public string AvatarUrl { get; set; } + public DateTime FirstLoginTime { get; set; } + public bool IsDisabled { get; set; } + public DateTime CreateTime { get; set; } + public DateTime UpdateTime { get; set; } + public string Token { get; set; } + } +} diff --git a/WxCheckApi/Controllers/WeatherForecastController.cs b/WxCheckApi/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..80548eb --- /dev/null +++ b/WxCheckApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; + +namespace WxCheckApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} diff --git a/WxCheckApi/Program.cs b/WxCheckApi/Program.cs new file mode 100644 index 0000000..de9a6f4 --- /dev/null +++ b/WxCheckApi/Program.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using MySql.Data.MySqlClient; +using System.Text; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllers(); + +// 添加HttpClientFactory +builder.Services.AddHttpClient(); + +// 添加数据库连接 +builder.Services.AddScoped(sp => { + var connectionString = builder.Configuration.GetConnectionString("MySQLConnection"); + return new MySqlConnection(connectionString); +}); + +builder.Services.AddAuthorization(); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(option => + { + string DefaultKey = "B,EZipeApY3cNj3~4RP0UMR=H>9x8.1!E85wmZ]]py2d$Y?5"; + var sec = Encoding.UTF8.GetBytes(builder.Configuration["JWT:SecretKey"] ?? DefaultKey); + + option.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["JwT:Issuer"], + ValidAudience = builder.Configuration["JwT:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(sec) + }; + + option.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var token = context.Request.Headers["token"].FirstOrDefault(); + if (string.IsNullOrEmpty(token)) + { + // 如果没有找到 token 头部,则继续检查 Authorization 头部 + token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + } + // 如果找到了 token,则将其设置到 HttpContext 中 + if (!string.IsNullOrEmpty(token)) + { + context.Token = token; + } + return Task.CompletedTask; + } + }; + }); + + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.UseAuthentication(); // 添加认证中间件 +app.UseAuthorization(); // 使用授权中间件 + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/WxCheckApi/Properties/PublishProfiles/FolderProfile.pubxml b/WxCheckApi/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..ebb3203 --- /dev/null +++ b/WxCheckApi/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,20 @@ + + + + + true + false + true + Release + Any CPU + FileSystem + bin\Release\net8.0\publish\ + FileSystem + <_TargetId>Folder + + net8.0 + win-x64 + e545c738-a21b-71f3-9fb9-a68d8018822d + false + + \ No newline at end of file diff --git a/WxCheckApi/Properties/PublishProfiles/FolderProfile.pubxml.user b/WxCheckApi/Properties/PublishProfiles/FolderProfile.pubxml.user new file mode 100644 index 0000000..8da1dc5 --- /dev/null +++ b/WxCheckApi/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -0,0 +1,9 @@ + + + + + <_PublishTargetUrl>E:\Project\WxCheck\WxCheckApi\bin\Release\net8.0\publish\ + True|2025-11-08T01:48:12.8385703Z||;True|2025-11-07T15:09:26.3902793+08:00||;True|2025-11-07T11:51:31.5643958+08:00||;True|2025-11-07T11:50:44.2146653+08:00||;True|2025-11-04T14:29:55.7628000+08:00||;False|2025-11-04T14:29:50.0660192+08:00||;True|2025-11-04T14:20:52.6380538+08:00||;True|2025-11-04T10:14:31.0406197+08:00||;True|2025-11-04T10:01:51.9971474+08:00||;True|2025-11-04T09:53:16.2195499+08:00||;True|2025-11-04T09:45:30.2430508+08:00||;True|2025-11-04T08:52:40.6424728+08:00||;True|2025-11-03T21:04:36.1464303+08:00||;True|2025-11-03T20:46:33.3635634+08:00||;True|2025-11-03T20:45:29.5368625+08:00||;True|2025-11-03T20:42:14.5975957+08:00||;True|2025-11-03T20:40:43.0062117+08:00||;True|2025-11-03T20:16:38.4711929+08:00||;True|2025-11-03T20:14:42.8936169+08:00||;True|2025-11-03T19:35:15.7051947+08:00||;True|2025-11-03T19:01:58.6546141+08:00||;True|2025-11-03T17:59:30.0861681+08:00||;True|2025-11-03T16:15:01.5245126+08:00||;True|2025-11-03T16:06:34.1178642+08:00||;True|2025-10-31T17:30:06.3039818+08:00||;True|2025-10-31T16:20:12.7490590+08:00||;True|2025-10-31T15:10:14.4751645+08:00||; + + + \ No newline at end of file diff --git a/WxCheckApi/Properties/launchSettings.json b/WxCheckApi/Properties/launchSettings.json new file mode 100644 index 0000000..813d77d --- /dev/null +++ b/WxCheckApi/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6041", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "http://localhost:5065", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WxCheckApi/WeatherForecast.cs b/WxCheckApi/WeatherForecast.cs new file mode 100644 index 0000000..68c85aa --- /dev/null +++ b/WxCheckApi/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace WxCheckApi +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/WxCheckApi/WxCheckApi.csproj b/WxCheckApi/WxCheckApi.csproj new file mode 100644 index 0000000..2f1bbef --- /dev/null +++ b/WxCheckApi/WxCheckApi.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/WxCheckApi/WxCheckApi.http b/WxCheckApi/WxCheckApi.http new file mode 100644 index 0000000..d2802ef --- /dev/null +++ b/WxCheckApi/WxCheckApi.http @@ -0,0 +1,6 @@ +@WxCheckApi_HostAddress = http://localhost:5065 + +GET {{WxCheckApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/WxCheckApi/WxCheckApi.slnx b/WxCheckApi/WxCheckApi.slnx new file mode 100644 index 0000000..fb240d7 --- /dev/null +++ b/WxCheckApi/WxCheckApi.slnx @@ -0,0 +1,3 @@ + + + diff --git a/WxCheckApi/appsettings.Development.json b/WxCheckApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/WxCheckApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WxCheckApi/appsettings.json b/WxCheckApi/appsettings.json new file mode 100644 index 0000000..c12022d --- /dev/null +++ b/WxCheckApi/appsettings.json @@ -0,0 +1,25 @@ +{ + "JwT": { + "SecretKey": "1%猜U36eraIYI?3s9dI}46an不Nn>P]3)$9:dCnS5=ajAu%8B5]15hF到20T20QBD]Mt9}2z76jO#Glg&0yDy7k-2zVdt&Z5ur>=l)QF2^1&Dq04m76U2P9wvlWf", + "Issuer": "微信小程序token", + "Audience": "W*u93xxp*08DnW@%6}5Tjh6bE?;hW" + }, + "WeChat": { + "AppId": "wx42e9add0f91af98b", + "AppSecret": "5620f00b40297efaf3d197d61ae184d6" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "MySQLConnection": "Server=47.119.147.104;Database=wx_xcx_check;user id=root;password=hbfjW6A_eob;port=3307;" + }, + "AmapApi": { + "ApiKey": "4d5cb7818664ada68ae5f68783b8bd4c" + } + +} diff --git a/WxCheckApi/wx_xcx_check.sql b/WxCheckApi/wx_xcx_check.sql new file mode 100644 index 0000000..8b56a72 --- /dev/null +++ b/WxCheckApi/wx_xcx_check.sql @@ -0,0 +1,83 @@ +/* + Navicat Premium Dump SQL + + Source Server : 47.119.147.104 + Source Server Type : MariaDB + Source Server Version : 120002 (12.0.2-MariaDB) + Source Host : 47.119.147.104:3307 + Source Schema : wx_xcx_check + + Target Server Type : MariaDB + Target Server Version : 120002 (12.0.2-MariaDB) + File Encoding : 65001 + + Date: 07/11/2025 15:03:55 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for xcx_conversation +-- ---------------------------- +DROP TABLE IF EXISTS `xcx_conversation`; +CREATE TABLE `xcx_conversation` ( + `Id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `UserKey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户唯一标识键', + `ConversationContent` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '会话内容', + `MessageType` int(11) NOT NULL DEFAULT 1 COMMENT '信息类型:1-公有,2-私有', + `SendMethod` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送方式', + `UserLocation` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '用户定位信息', + `RecordTime` datetime NOT NULL COMMENT '记录时间', + `RecordTimeUTCStamp` bigint(20) NOT NULL COMMENT '记录时间的UTC时间戳', + `IsDeleted` tinyint(4) NULL DEFAULT 0 COMMENT '是否删除:0-正常,1-删除', + `CreateTime` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间', + `Latitude` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '纬度', + `Longitude` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '经度', + PRIMARY KEY (`Id`) USING BTREE, + INDEX `idx_userkey`(`UserKey` ASC) USING BTREE, + INDEX `idx_utcstamp`(`RecordTimeUTCStamp` ASC) USING BTREE, + INDEX `idx_deleted`(`IsDeleted` ASC) USING BTREE, + INDEX `idx_recordtime`(`RecordTime` ASC) USING BTREE, + INDEX `idx_messagetype`(`MessageType` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 57 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会话记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for xcx_log +-- ---------------------------- +DROP TABLE IF EXISTS `xcx_log`; +CREATE TABLE `xcx_log` ( + `Id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `RecordTime` datetime NOT NULL COMMENT '记录时间', + `UserKey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户唯一标识键', + `OperationContent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作内容', + `UserLocation` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '用户定位信息', + `Impact` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '产生的影响', + `CreateTime` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间', + PRIMARY KEY (`Id`) USING BTREE, + INDEX `idx_userkey`(`UserKey` ASC) USING BTREE, + INDEX `idx_recordtime`(`RecordTime` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for xcx_users +-- ---------------------------- +DROP TABLE IF EXISTS `xcx_users`; +CREATE TABLE `xcx_users` ( + `Id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `UserName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户名', + `UserKey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户唯一标识键', + `WeChatName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信名称', + `PhoneNumber` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电话号码', + `FirstLoginTime` datetime NOT NULL COMMENT '首次登录时间', + `IsDisabled` tinyint(4) NULL DEFAULT 0 COMMENT '是否禁用:0-启用,1-禁用', + `CreateTime` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间', + `UpdateTime` datetime NULL DEFAULT current_timestamp() ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `AvatarUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像地址', + PRIMARY KEY (`Id`) USING BTREE, + UNIQUE INDEX `idx_userkey`(`UserKey` ASC) USING BTREE, + INDEX `idx_disabled`(`IsDisabled` ASC) USING BTREE, + INDEX `idx_logintime`(`FirstLoginTime` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '小程序用户表' ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1;