1
0
Code Issues Pull Requests Packages Projects Releases Wiki Activity GitHub Gitee

25 Commits

Author SHA1 Message Date
be6919124d bugfix 2023-04-26 01:14:29 +08:00
2d145793b4 小程序端审核隐藏功能通过传参指定 2023-04-26 01:01:18 +08:00
d96fd3d536 update 2023-04-26 00:34:24 +08:00
0dc02ae454 bugfix 门禁端electron嵌入网页地址错误;小改动 2023-04-25 16:46:53 +08:00
83047d7877 小程序小商店同时可以搜商品名称和商品图片;后台管理显示商品简介;其他一些小调整 2023-04-25 05:48:47 +08:00
85472f43e8 Gateway中的后台管理前端重新打包 2023-04-25 04:45:27 +08:00
8ff95cdc07 后台管理分成两个端 2023-04-25 04:41:40 +08:00
c2a914d7f9 门禁端左侧提示文字修改 2023-04-25 04:38:01 +08:00
2038d76b1e bugfix 修复若干商品管理Bug;小程序商品列表不展示下架商品 2023-04-25 04:33:38 +08:00
0bcef8d497 导出SQL 2023-04-25 02:57:34 +08:00
9a0fe1715e bugfix 后台管理商品无法修改的问题 2023-04-25 02:46:37 +08:00
482d91eba3 小修改 2023-04-25 02:26:45 +08:00
95a485cfc4 项目需要配置的地方添加 FIXME 环境配置 注释方便查找 2023-04-25 02:17:19 +08:00
64b4fe31a3 bugfix 修复开启多个门禁websocket频繁断开问题;bugfix仅扫描门禁显示提示窗 2023-04-25 02:04:40 +08:00
65563f5b75 小程序扫码门禁端显示详情 2023-04-25 01:41:23 +08:00
b68fde365f 修复小程序获取小程序码失败的问题 2023-04-25 01:40:05 +08:00
d3fb7827f1 后台管理修改密码 2023-04-24 22:45:57 +08:00
9a8f3d050c 添加管理员后台取消订单并退款功能;订单发货、取消订单添加二次确认弹窗 2023-04-24 22:34:00 +08:00
2d82571303 Gateway添加通过IP限流 2023-04-24 19:01:57 +08:00
2f469aec14 门禁端网页不允许鼠标选中 2023-04-24 01:08:22 +08:00
83f424b80f 后台管理 -> 订单管理完成;nginx配置文件限制ip并发数 2023-04-23 00:52:43 +08:00
152ff7d8e5 后台管理登录时判断用户是否有权登入系统;隐藏右上角消息提示按钮 2023-04-22 17:27:53 +08:00
f5bc5b9eef 后端添加域名访问首页入口单页;前端打包塞入Gateway中 2023-04-22 17:18:24 +08:00
d4edba9212 更新项目文档;一些小改动 2023-04-19 01:03:54 +08:00
68217b81dc bugfix 小程序 商品列表页面划线价零售价写反了 2023-04-18 23:03:52 +08:00
91 changed files with 2866 additions and 1325 deletions

60
20230425-epp.md Normal file
View File

@@ -0,0 +1,60 @@
# 基于微服务的社区疫情防控系统
Epidemic prevention platform
> 项目首页:
>
> https://epp.only4.work/
>
> 代码仓库地址:
>
> https://git.only4.work/coder-xiaomo/epp
本项目采用 monorepo 单仓库模式进行维护,项目完整代码均在此仓库中。
## 管理员后台
> 管理员后台地址:
>
> https://epp.only4.work/manage/index.html
社区管理员 测试账号admin<span style="user-select: none;"> </span>admin
系统管理员 测试账号root<span style="user-select: none;"> </span>root
## 门禁端
>网页版地址:
>
>https://epp.only4.work/guard/index.html
可以直接用 **微信扫一扫功能****小程序端“扫门禁码”功能** 扫描门禁码,**扫描**及**确认进门**操作门禁端会弹出成功提示
\* 跨端桌面应用界面和功能均与网页版一致
## 小程序
> 小程序已审核上线,可以搜索 **devprogram** 或者扫描下方小程序码
\* 由于生活物资、进出码无法审核通过,所以**提审时隐藏了部分功能入口**。
如果登录之后**下方只有体温上报功能**,可以**点击右上角三个点→重新进入小程序**即可展示全部功能。
![](https://epp.only4.work/assets/image/miniprogram.jpg)
<br>
-----
<div style="text-align: center;">
张博凯
</div>

252
JMeter测试-HTTP请求.jmx Normal file
View File

@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.4.3">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="测试计划" enabled="true">
<stringProp name="TestPlan.comments"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<intProp name="LoopController.loops">-1</intProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">10</stringProp>
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">epp.only4.work</stringProp>
<stringProp name="HTTPSampler.port">80</stringProp>
<stringProp name="HTTPSampler.protocol">https</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">/user/manage/getUserList</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="响应断言" enabled="true">
<collectionProp name="Asserion.test_strings"/>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">16</intProp>
</ResponseAssertion>
<hashTree/>
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="察看结果树" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
<ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
</hashTree>
</hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<intProp name="LoopController.loops">-1</intProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">800</stringProp>
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">80</stringProp>
<stringProp name="HTTPSampler.protocol">http</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">/access/code/getCodeInfo?id=1</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">true</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
<stringProp name="HTTPSampler.connect_timeout">300000</stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="响应断言" enabled="true">
<collectionProp name="Asserion.test_strings"/>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">16</intProp>
</ResponseAssertion>
<hashTree/>
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="察看结果树" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
<ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>

5
MoveBackendJar.bat Normal file
View File

@@ -0,0 +1,5 @@
move .\backend\microservice-gateway\target\microservice-gateway-0.0.1-SNAPSHOT.jar .\ignore
move .\backend\microservice-provider-user-8001\target\microservice-provider-user-8001-0.0.1-SNAPSHOT.jar .\ignore
move .\backend\microservice-provider-access-8002\target\microservice-provider-access-8002-0.0.1-SNAPSHOT.jar .\ignore
move .\backend\microservice-provider-shop-8003\target\microservice-provider-shop-8003-0.0.1-SNAPSHOT.jar .\ignore
pause

192
README.md
View File

@@ -4,6 +4,8 @@
代码仓库:[GitHub](https://github.com/coder-xiaomo/epp) [Gitee](https://gitee.com/coder-xiaomo/epp) [自建](https://git.only4.work/coder-xiaomo/epp)
本项目采用 monorepo 单仓库模式进行维护,项目完整代码均在此仓库中。
## 简介
@@ -122,6 +124,9 @@ git clone https://github.com/coder-xiaomo/epp
# git clone https://git.only4.work/coder-xiaomo/epp
# 进入项目根目录
cd epp
# 切换到 production 分支
git checkout production
```
@@ -212,9 +217,7 @@ cd ../
# ############
# 进入 frontend 目录
cd frontend
# 建议使用 cnpm 安装依赖,如果没有安装 cnpm可以通过 npm i cnpm 进行安装
cnpm install
# npm install --legacy-peer-deps
npm install
# 回到项目根目录下
cd ../
```
@@ -304,20 +307,42 @@ npm run package
#### 管理员前端项目打包并嵌入后端
```
cd frontend
npm run build
```
然后将 `frontend/dist` 文件夹移动到 `backend/microservice-gateway/src/main/resources/static` 文件夹下,并修改文件夹名称 `dist``manage`
#### 后端配置
##### 配置业务域名
TODO
修改 `backend\microservice-provider-access-8002\src\main\resources\static\access\assets\js\websocket.js` 文件
修改 `backend/microservice-provider-access-8002/src/main/resources/static/access/renderer.js` 文件
```
window.wsUrl = 'ws://127.0.0.1:80/access/websocket/1';
const baseUrl = "https://【⚠此处修改为你的业务域名】/" // 以 / 结尾例如https://epp.only4.work/
// 或者也可使用相对路径,例如:
const baseUrl = "/"
```
修改 `backend/microservice-provider-access-8002/src/main/resources/static/access/assets/js/websocket.js` 文件
```
window.wsUrl = 'ws://【⚠此处修改为你的业务域名】/access/websocket/';
```
> 注意,如果使用了 SSL 证书,那么 ws:// 要换成 wss://
##### 配置小程序 APPID 与 APPSECRET
修改 `backend/microservice-provider-access-8002/src/main/java/com/cxyxiaomo/epp/access/service/WeChatTokenServiceImpl.java` 文件
@@ -330,9 +355,38 @@ window.wsUrl = 'ws://127.0.0.1:80/access/websocket/1';
##### 配置门禁端生成的小程序码环境
修改 `backend/microservice-provider-access-8002/src/main/resources/static/access/renderer.js` 文件
```
const envVersion = "【⚠此处修改为当前小程序环境】" // 正式版为 "release",体验版为 "trial",开发版为 "develop"
```
##### 配置 Gateway 限流策略(可选)
> 如果你不懂这是在做什么,请直接跳过这一步
修改 `backend/microservice-gateway/src/main/resources/application.yml` 文件中 `routes` 中各个微服务的 `filters` 例如:
```yml
filters: # 路由过滤器,使用自定义的限流过滤器工厂
- name: RateLimitByIp # 设置每秒允许5个请求每次请求需要1个令牌
args:
rate: 5.0
permits: 1
```
##### 打 jar 包
IDEA 中右侧 Maven 双击 Lifestyle 的 package打包完成后的 jar 包可在以下位置找到
> 打包需要一起打包,每个项目单独打包可能会提示找不到 microservice-common 相关依赖
IDEA 中右侧 Maven 双击 Lifestyle 的 package建议先 clean 一下,并点击选中 Maven 的跳过测试,再进行 package打包完成后的 jar 包可在以下位置找到
- backend/microservice-gateway/target/microservice-gateway-0.0.1-SNAPSHOT.jar
- backend/microservice-provider-user-8001/target/microservice-provider-user-8001-0.0.1-SNAPSHOT.jar
- backend/microservice-provider-access-8002/target/microservice-provider-access-8002-0.0.1-SNAPSHOT.jar
@@ -340,22 +394,70 @@ IDEA 中右侧 Maven 双击 Lifestyle 的 package打包完成后的 jar 包
#### nginx 代理配置(可选)
#### Nacos 配置
> nacos 后台地址http://127.0.0.1:8488/nacos/index.html
1. 进入 ,修改 nacos 登录密码(可选)
> 左侧 **权限控制** → **用户列表** → **修改**
2. 修改 nacos 端口为 8488与后端项目配置文件中保持一致
> nacos/conf/application.properties 中的端口号 port 改为 8488
3. 创建两个命名空间命名空间ID分别为 `develop``production`
> 左侧 **命名空间** → **新建命名空间**
| 命名空间名称 | 命名空间ID | 描述 | 配置数 | 操作 |
| :--------------- | :--------- | :--------- | :----- | :----------- |
| public(保留空间) | | | 0 | 详情删除编辑 |
| develop | develop | develop | 0 | 详情删除编辑 |
| production | production | production | 0 | 详情删除编辑 |
##### 新版 Nacos 需要配置密钥
**若使用的 Nacos 版本小于或等于 Nacos 2.2.0,不需要进行如下配置。**
🌟若使用的 Nacos 版本大于或等于 Nacos 2.2.0.1,则需要配置自定义密钥:
```properties
# 密钥值可以自己指定,此处的自定义密钥来自 nacos 官网
nacos.core.auth.plugin.nacos.token.secret.key=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg=
```
参考https://nacos.io/zh-cn/docs/v2/guide/user/auth.html
#### nginx 代理配置
##### 反向代理(可选)
配置文件在 `nginx-conf` 目录下(不能直接拿来用,需要根据自己的实际情况来改)
> 以下配置仅供参考
```conf
server
{
server_name epp.only4.work; # ⚠ Api 业务域名
server_name epp.only4.work; # ⚠ Api 业务域名
listen 80;
listen 443 ssl http2;
# 并发限制 限制当前站点最大并发数
limit_conn perserver 50;
# 单IP限制 限制单个IP访问最大并发数
limit_conn perip 10;
# 流量限制 限制每个请求的流量上限单位KB
limit_rate 8192k;
# 核心配置
location / {
proxy_pass http://127.0.0.1:5203; # ⚠ Gateway 微服务项目本地运行的端口
# websocket
# websocket # ⚠ 门禁端用户扫码后通过 websocket 推送到门禁端完成开门
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
@@ -367,7 +469,9 @@ server
}
```
##### SSL证书配置可选
如果域名配置为 https:// 则需要配置 SSL 证书。SSL证书可在 nginx 中进行配置,具体配置方法此处省略。
@@ -421,23 +525,64 @@ Nacos 后台地址为http://localhost:8848/nacos/index.html默认用户名
#### [部署&开发] 启动 MySQL
```
# Ubuntu 下
sudo systemctl start mysql
```
#### [部署&开发] 启动各个微服务
开发:直接在 IDEA 中启动即可
开发环境Windows下
部署:
> 直接在 IDEA 中启动即可
>
> 若系统内存够用,可以适当调大内存参数,如:-Xmx1024M -Xms256M
```
# 启动 UserProvider
```bash
# 启动 Gateway
# ......
java -jar -Xmx512M -Xms128M -Dspring.profiles.active=develop ./microservice-gateway-0.0.1-SNAPSHOT.jar
# 启动 UserProvider
java -jar -Xmx512M -Xms128M -Dspring.profiles.active=develop ./microservice-provider-user-8001-0.0.1-SNAPSHOT.jar
# 启动 AccessProvider
java -jar -Xmx512M -Xms128M -Dspring.profiles.active=develop ./microservice-provider-access-8002-0.0.1-SNAPSHOT.jar
# 启动 ShopProvider
java -jar -Xmx512M -Xms128M -Dspring.profiles.active=develop ./microservice-provider-shop-8003-0.0.1-SNAPSHOT.jar
```
生产环境Ubuntu下
> 假设生产环境已有如下配置:
>
> - JDK 中 java 可执行文件所在路径为:/www/wwwroot/env/jdk-11/bin/java
> - jar 包所在路径为:/www/wwwroot/workspace/
>
> 若系统内存够用,可以适当调大内存参数,如:-Xmx1024M -Xms256M
```bash
# 启动 Gateway
/www/wwwroot/env/jdk-11/bin/java -jar -Xmx512M -Xms128M -Dspring.profiles.active=production -Dserver.port=8000 /www/wwwroot/workspace/microservice-gateway-0.0.1-SNAPSHOT.jar
# 启动 UserProvider
/www/wwwroot/env/jdk-11/bin/java -jar -Xmx512M -Xms128M -Dspring.profiles.active=production /www/wwwroot/workspace/microservice-provider-user-8001-0.0.1-SNAPSHOT.jar
# 启动 AccessProvider
/www/wwwroot/env/jdk-11/bin/java -jar -Xmx512M -Xms128M -Dspring.profiles.active=production /www/wwwroot/workspace/microservice-provider-access-8002-0.0.1-SNAPSHOT.jar
# 启动 ShopProvider
/www/wwwroot/env/jdk-11/bin/java -jar -Xmx512M -Xms128M -Dspring.profiles.active=production /www/wwwroot/workspace/microservice-provider-shop-8003-0.0.1-SNAPSHOT.jar
```
#### [可选] 启动 nginx
```bash
# Ubuntu 下
sudo systemctl start nginx
```
@@ -461,6 +606,17 @@ npm run serve
## 项目相关地址
### 门禁端网页
https://epp.only4.work/access/index.html
Nacos 注册中心
http://106.75.217.14:8488/nacos/index.html
## 停止项目
这个应该不用多说吧

164
TODOs.md
View File

@@ -1,77 +1,139 @@
还要做的部分
小程序提审要做的:
小程序扫门禁码之后门禁开门(扫码 websocket 推到门禁端),小程序端显示开门成功
项目部署到服务器
小程序端用户头像
大门 人员进出 后台管理(进出日志)
后台订单管理页
每个人创建账号的时候填充一些测试数据进去
门禁端左侧提示文字修改
### 还要做的部分
后台管理两个端分开
用户管理 社区管理员不能修改管理员账号
后台管理分两个管理员身份
- 不可以删除用户,只允许禁用用户
后台管理 按照id进行筛选
社区管理员管理人员进出权限
& 演示时快速创建账号
& 小程序提审(提审时隐藏小商店 调试按钮、上帝按钮)
& 发给老班看一看
发给老班看一看
数据库填充测试数据(以及创建一些测试账号)并备份,线上演示数据修改删除避免真删
项目中的TODO
### 可能会被问到的问题
商品库存问题
### 其他
小程序端用户头像
大门 人员进出 后台管理(进出日志)
每个人创建账号的时候填充一些测试数据进去
后台管理 按照id进行筛选
更多:
完成项目代码中的 TODO 部分
Java代码中小程序AppID、密钥处理小程序代码中小程序AppID处理
# VSCode 全局搜索排除
node_modules,.git,.idea,target,out,./postman-collection,@deprecated
# host 文件地址
C:\Windows\System32\drivers\etc
配置项
127.0.0.1 epp.only4.work
查看配置
ping epp.only4.work
# 后端项目启动命令
```bash
; /www/wwwroot/env/jdk-11/bin/java -jar -Xmx1024M -Xms256M /www/wwwroot/workspace/microservice-gateway-0.0.1-SNAPSHOT.jar --server.port=7087
/www/wwwroot/env/jdk-11/bin/java -jar -Xmx512M -Xms128M -Dspring.profiles.active=production -Dserver.port=8000 /www/wwwroot/workspace/microservice-gateway-0.0.1-SNAPSHOT.jar
# 小程序提审期间
# /www/wwwroot/env/jdk-11/bin/java -jar -Xmx512M -Xms128M -Dspring.profiles.active=production -Dserver.port=8000 -Depp.miniprogram.config=hide /www/wwwroot/workspace/microservice-gateway-0.0.1-SNAPSHOT.jar
/www/wwwroot/env/jdk-11/bin/java -jar -Xmx512M -Xms128M -Dspring.profiles.active=production /www/wwwroot/workspace/microservice-provider-user-8001-0.0.1-SNAPSHOT.jar
/www/wwwroot/env/jdk-11/bin/java -jar -Xmx512M -Xms128M -Dspring.profiles.active=production /www/wwwroot/workspace/microservice-provider-access-8002-0.0.1-SNAPSHOT.jar
/www/wwwroot/env/jdk-11/bin/java -jar -Xmx512M -Xms128M -Dspring.profiles.active=production /www/wwwroot/workspace/microservice-provider-shop-8003-0.0.1-SNAPSHOT.jar
java -jar -Xmx512M -Xms128M -Dspring.profiles.active=develop ./microservice-gateway-0.0.1-SNAPSHOT.jar
java -jar -Xmx512M -Xms128M -Dspring.profiles.active=develop ./microservice-provider-user-8001-0.0.1-SNAPSHOT.jar
java -jar -Xmx512M -Xms128M -Dspring.profiles.active=develop ./microservice-provider-access-8002-0.0.1-SNAPSHOT.jar
java -jar -Xmx512M -Xms128M -Dspring.profiles.active=develop ./microservice-provider-shop-8003-0.0.1-SNAPSHOT.jar
```
# BT宝塔登录密钥
==================================================================
外网面板地址: http://106.75.217.14:18734/107bb99c
内网面板地址: http://10.23.189.141:18734/107bb99c
username: qmud57rc
password: 80347b8d
If you cannot access the panel,
release the following panel port [18734] in the security group
若无法访问面板,请检查防火墙/安全组是否有放行面板[18734]端口
==================================================================
项目开发中遇到的问题
小程序双端不一致的问题参数转义页面CSS样式是不完全一样的。
# IP 配置
develop
内网穿透106.75.217.14
nacoshttp://106.75.217.14:8488/nacos/index.html
数据库:本地
production
nacoshttp://106.75.217.14:8488/nacos/index.html
数据库:服务器上
# QCloud
主账号ID 100014397291
用户名 epp
登录密码 -
SecretId AKIDSkmeXTHsTqzwe8ZDiGcomW4OYXcZIerp
SecretKey 22sVt494mGZeV7sQkqwxnNjneHesqXxA
# 门禁端网页
https://epp.only4.work/access/index.html
https://epp-prod.only4.work/access/index.html
# 启动远程 Nacos
> /www/wwwserv/epp/nacos/conf/application.properties port 改为 8488
```
cd /www/wwwserv/epp/nacos/bin
bash startup.sh -m standalone
bash shutdown.sh
```
http://106.75.217.14:8488/nacos/index.html
nacos
@@ -81,51 +143,41 @@ socan
# 启动本地 Nacos
```bash
E:
cd E:\nacos\bin
startup.cmd -m standalone
```
# 内网穿透
```bash
cd E:\Project\毕业设计\epp\intranet-penetration\bin
rathole.exe ../conf/client.toml
```
# 微信小程序后台设置 小程序最低基础库 2.21.3
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html
**项目亮点**
优点:
订单id使用 雪花id可以分布式部署
遇到的问题:
**项目开发中遇到的问题**
nacos CPU占满问题解决方案提issue多次测试找到问题HTTPDebug问题解决
iPhone部分版本不支持webp格式图片
小程序双端不一致的问题参数转义页面CSS样式是不完全一样的。
-----
Nacos CPU 跑满问题 我提的 GitHub issue
https://github.com/alibaba/nacos/issues/10080
-----
Nacos 2.2.0.1 需要配置
nacos.core.auth.plugin.nacos.token.secret.key=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg=
参考:
https://nacos.io/zh-cn/docs/v2/guide/user/auth.html
-----
体温填报接口:
今日是否填报过,上报体温,上报的体温的历史记录
更多:
完成项目代码中的 TODO 部分
身份码后端接口考虑与其他系统的集成逻辑
Java代码中小程序AppID、密钥处理小程序代码中小程序AppID处理
https://github.com/alibaba/nacos/issues/10080

View File

@@ -0,0 +1,19 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Dev-Gateway (epp.miniprogram.config=hide)" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<envs>
<env name="--spring.profiles.active" value="develop" />
</envs>
<module name="microservice-gateway" />
<option name="SPRING_BOOT_MAIN_CLASS" value="com.cxyxiaomo.epp.gateway.Gateway" />
<option name="VM_PARAMETERS" value="-Xms64m -Xmx256m -Depp.miniprogram.config=hide" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.cxyxiaomo.epp.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -3,6 +3,7 @@ package com.cxyxiaomo.epp.PageTable.enums;
public enum AddType {
CAN_NOT_ADD("plainText"),
INPUT("input"),
INPUT_NUMBER("input-number"),
TEXTAREA("textarea"),
SELECT("select"),
IMAGE("image");

View File

@@ -3,6 +3,7 @@ package com.cxyxiaomo.epp.PageTable.enums;
public enum EditType {
CAN_NOT_EDIT("plainText"),
INPUT("input"),
INPUT_NUMBER("input-number"),
TEXTAREA("textarea"),
SELECT("select"),
IMAGE("image");

View File

@@ -43,11 +43,12 @@ public class FieldRuleBuilder {
return this;
}
public FieldRuleBuilder number() {
rules.put("type", "number");
rules.put("message", fieldDisplayName + "必须为数字");
return this;
}
// public FieldRuleBuilder number() {
// // rules.put("type", "number"); // 字符串类型的数字匹配不上
// rules.put("regexp", "^\\d+?$");
// rules.put("message", fieldDisplayName + "必须为数字");
// return this;
// }
public FieldRuleBuilder min(Integer min) {
rules.put("min", min);
@@ -61,6 +62,7 @@ public class FieldRuleBuilder {
return this;
}
// 字符串类型长度范围
public FieldRuleBuilder minMax(Integer min, Integer max) {
rules.put("min", min);
rules.put("max", max);
@@ -68,6 +70,28 @@ public class FieldRuleBuilder {
return this;
}
// 数字类型数值范围
// public FieldRuleBuilder range(Integer min, Integer max) {
// // rules.put("min", min);
// // rules.put("max", max);
// rules.put("message", fieldDisplayName + "应大于 " + min + " 且小于 " + max);
// return this;
// }
// // 数字类型数字个数
// public FieldRuleBuilder price(Integer minLen, Integer maxLen) {
// rules.put("regexp", "^\\d{" + minLen + "," + maxLen + "}(\\.\\d{1,2})?$");
// rules.put("message", fieldDisplayName + "应大于等于 " + Math.pow(10, minLen - 1) + " ,小于等于 " + (Math.pow(10, maxLen) - 1) + " 且小数位数不超过 2 位");
// return this;
// }
// public FieldRuleBuilder regexp(String regexp) {
// // rules.put("type", "regexp");
// rules.put("pattern", regexp);
// rules.put("message", fieldDisplayName + "输入不符合要求,请检查");
// return this;
// }
public FieldRuleBuilder length(Integer len) {
rules.put("min", len);
rules.put("max", len);

View File

@@ -4,7 +4,7 @@ import java.util.Arrays;
import java.util.Optional;
public enum OrderStatus {
PENDING("Pending", "等待确认"),
PENDING("Pending", "等待支付"),
PROCESSING("Processing", "已支付,等待发货中"),
SHIPPED("Shipped", "已发货,等待确认收货"),
DELIVERED("Delivered", "已送达"),

View File

@@ -4,10 +4,12 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@NoArgsConstructor
@Accessors(chain = true) // 链式写法
public class Good {
public class Good implements Serializable {
Long id;
String goodsName;
Integer categoryId;

View File

@@ -4,10 +4,12 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@NoArgsConstructor
@Accessors(chain = true) // 链式写法
public class GoodCategory {
public class GoodCategory implements Serializable {
Long id;
String categoryName;
Integer order;

View File

@@ -4,13 +4,14 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@Accessors(chain = true) // 链式写法
public class Order {
public class Order implements Serializable {
private Long id;
private Integer userId;
private LocalDateTime orderDate;

View File

@@ -4,10 +4,12 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@NoArgsConstructor
@Accessors(chain = true) // 链式写法
public class OrderDetail {
public class OrderDetail implements Serializable {
private Long id;
private Long orderId;
private Long goodId;

View File

@@ -0,0 +1,18 @@
package com.cxyxiaomo.epp.common.query;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@NoArgsConstructor
@Accessors(chain = true) // 链式写法
// 微服务必须要实现Serializable
public class OrderQuery implements Serializable {
private Long id;
private Integer userId;
private String orderStatusCode;
private String expressId;
}

View File

@@ -9,7 +9,6 @@ import org.springframework.beans.BeanUtils;
import java.io.Serializable;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@@ -22,10 +21,10 @@ import java.util.stream.Collectors;
public class OrderVO implements Serializable {
private String id;
private Integer userId;
private LocalDateTime orderDate;
private String orderStatus;
private String orderStatusCode;
private String orderPrice;
private String orderDate;
private String payDate;
private String cancelDate;
private String shipDate;
@@ -47,6 +46,9 @@ public class OrderVO implements Serializable {
String price = order.getOrderPrice().setScale(2, RoundingMode.FLOOR).toPlainString();
orderVO.setOrderPrice(price);
if (order.getOrderDate() != null) {
orderVO.setOrderDate(order.getOrderDate().toString().replace("T", " "));
}
if (order.getPayDate() != null) {
orderVO.setPayDate(order.getPayDate().toString().replace("T", " "));
}

View File

@@ -0,0 +1,79 @@
package com.cxyxiaomo.epp.gateway.Factory;
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
// 一个自定义的限流过滤器工厂
public class RateLimitByIpGatewayFilterFactory extends AbstractGatewayFilterFactory<RateLimitByIpGatewayFilterFactory.Config> {
// 用于存储IP地址和对应的计数器
private static final Map<String, RateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();
public RateLimitByIpGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// 获取请求的IP地址
ServerHttpRequest request = exchange.getRequest();
String ip = request.getRemoteAddress().getAddress().getHostAddress();
// 根据IP地址获取对应的限流器
RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(ip);
if (rateLimiter == null) {
// 如果不存在,则创建一个新的限流器,并放入缓存中
rateLimiter = RateLimiter.create(config.getRate());
RATE_LIMITER_CACHE.put(ip, rateLimiter);
}
// 判断请求是否被限流
if (rateLimiter.tryAcquire(config.getPermits())) {
// 如果没有被限流,则放行
return chain.filter(exchange);
} else {
System.out.println("限流ip: " + ip);
// 如果被限流则返回429状态码Too Many Requests
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return response.setComplete();
}
};
}
// 配置类,用于接收配置参数
public static class Config {
// 每秒允许的请求数
private double rate;
// 每次请求需要的令牌数
private int permits;
public double getRate() {
return rate;
}
public void setRate(double rate) {
this.rate = rate;
}
public int getPermits() {
return permits;
}
public void setPermits(int permits) {
this.permits = permits;
}
}
}

View File

@@ -1,10 +1,13 @@
package com.cxyxiaomo.epp.gateway.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Objects;
@RestController
public class Controller {
@@ -14,17 +17,29 @@ public class Controller {
return "[ERROR] 500 Internal Server Error";
}
@Value("${epp.miniprogram.config}")
private String miniprogramConfig;
/**
* 为了通过微信的小程序审核所特别处理的
*
* @return
*/
@RequestMapping("/getConfig")
public HashMap<String, Object> WxMiniProgramAuditSpecialHandle() {
boolean showCode = false;
boolean showShop = false;
public HashMap<String, Object> WxMiniProgramAuditSpecialHandle(@RequestParam(value = "v", required = false) String version) {
boolean showCode = true;
boolean showShop = true;
boolean showReport = true;
// 第一版提审,已通过,在线上 version == null
if (Objects.equals(version, "2")) {
// 第一版提审,按照配置文件中的配置来
if (Objects.equals(miniprogramConfig, "hide")) {
showCode = false;
showShop = false;
}
}
// 底部 tabbar
LinkedList<String> tabbarItem = new LinkedList<>();
tabbarItem.push("pages/index/index");

View File

@@ -40,20 +40,38 @@ spring:
predicates:
- Path=/user/**
- Method=GET,POST
filters: # 路由过滤器,使用自定义的限流过滤器工厂
- name: RateLimitByIp # 设置每秒允许5个请求每次请求需要1个令牌
args:
rate: 10.0
permits: 1
- id: access
uri: lb://microservice-provider-access
predicates:
- Path=/access/**
- Method=GET,POST
filters: # 路由过滤器,使用自定义的限流过滤器工厂
- name: RateLimitByIp # 设置每秒允许5个请求每次请求需要1个令牌
args:
rate: 10.0
permits: 1
- id: access-websocket
uri: lb:ws://microservice-provider-access
predicates:
- Path=/access/websocket/**
- id: shop
uri: lb://microservice-provider-shop
predicates:
- Path=/shop/**
- Method=GET,POST
filters: # 路由过滤器,使用自定义的限流过滤器工厂
- name: RateLimitByIp # 设置每秒允许5个请求每次请求需要1个令牌
args:
rate: 10.0
permits: 1
- id: test1
uri: lb://microservice-provider-test
@@ -79,3 +97,9 @@ spring:
args:
status: 302
url: https://www.baidu.com/?wd=
epp:
miniprogram:
# 为了通过微信的小程序审核所特别处理的
# show / hide
config: show

View File

@@ -1 +1,3 @@
编辑后右键 Compile And Reload File 修改即可生效,不用频繁重启项目
manage 文件夹下为 frontend 项目打包产物

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -0,0 +1,20 @@
.scan-result-container {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 20;
display: grid;
place-items: center;
}
.scan-result-image {
width: min(55vw, 55vh);
transition: 0.3s;
transform: scale(0.001);
}
.scan-result-image-show {
transform: scale(1);
}

View File

@@ -62,7 +62,7 @@
}
.hidden {
display: none;
display: none !important;
}
/* 弹窗列表样式 */
@@ -82,6 +82,7 @@
box-sizing: border-box;
place-items: center;
background-color: #f6f6f6;
cursor: pointer;
}
.gate-list-item > p {

View File

@@ -0,0 +1,39 @@
const URLScanSuccessImage = "./assets/svg/scan-success.svg"
const URLEnterGateImage = "./assets/svg/enter-gate.svg"
const DOMFullScreenMask = document.getElementById("full-screen-mask")
const DOMScanResultContainer = document.getElementById("scan-result-container")
const DOMQRCodeScanSuccessImage = document.getElementById("qrcode-scan-success")
var isShowing = false
function showResult(imageType) {
console.log("showResult")
if (isShowing) {
// console.log("showResult() skipped coz showing.")
// 等待 0.1s 重试
setTimeout(() => showResult(imageType), 100)
return
}
isShowing = true
DOMQRCodeScanSuccessImage.src = imageType === "onscan" ? URLScanSuccessImage : URLEnterGateImage
DOMFullScreenMask.classList.remove("hidden")
DOMScanResultContainer.classList.remove("hidden")
//
setTimeout(() => {
DOMQRCodeScanSuccessImage.classList.add("scan-result-image-show")
}, 100)
setTimeout(function hideResult() {
console.log("hideResult")
DOMQRCodeScanSuccessImage.classList.remove("scan-result-image-show")
setTimeout(() => {
DOMQRCodeScanSuccessImage.src = ""
DOMFullScreenMask.classList.add("hidden")
DOMScanResultContainer.classList.add("hidden")
isShowing = false
}, 300)
}, 3000)
}
// setTimeout(showResult, 100)
window.showResult = showResult

View File

@@ -1,6 +1,12 @@
window.wsUrl = 'wss://epp-prod.only4.work/access/websocket/1';
// window.wsUrl = 'ws://127.0.0.1:80/access/websocket/1';
// window.wsUrl = 'ws://127.0.0.1:8002/access/websocket/1';
/**
* FIXME 环境配置
*
* window.wsUrl
* - 线上环境'wss://epp.only4.work/access/websocket/'
* - 开发环境'ws://127.0.0.1:80/access/websocket/'
* 'ws://127.0.0.1:8002/access/websocket/'
*/
window.wsUrl = 'ws://127.0.0.1:80/access/websocket/';
window.ws = null; // WebSocket 实例对象
@@ -10,9 +16,20 @@ window.ws = null; // WebSocket 实例对象
return
}
function getWsUrl() {
function getUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
return `${window.wsUrl}${getUUID()}`
}
function createConn() {
// 创建webscoket 对象
const ws = new WebSocket(window.wsUrl)
const ws = new WebSocket(getWsUrl() /* window.wsUrl */)
// 执行上面的语句之后,客户端就会与服务器进行连接
// readyState返回当前实例对象的当前状态
@@ -34,9 +51,23 @@ window.ws = null; // WebSocket 实例对象
ws.onmessage = ({data}) => {
console.log('onmessage readyState', ws.readyState)
// 注意此时的data是json格式的 需要转化下
console.log('onmessage 有新消息啦=======>', JSON.parse(data))
let result = JSON.parse(data)
console.log('onmessage 有新消息啦=======>', result)
// 实例对象的send方法给服务器发送消息
ws.send('客户端发送的消息')
switch (result.action) {
case 'onscan':
case 'onopen':
if (window.currentGate && result.gateId === window.currentGate.id) {
console.log(result.action)
window.showResult(result.action)
}
break;
default:
console.log("switch=>default", result.action)
break;
}
}
// 实例对象的onclose属性用于连接关闭后的回调 函数
@@ -58,5 +89,6 @@ window.ws = null; // WebSocket 实例对象
}
return ws
}
window.ws = createConn()
})()

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100" height="100" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle cx="50" cy="50" r="45" stroke="white" stroke-width="7" fill="white"/>
<!-- 外围圆圈 -->
<circle cx="50" cy="50" r="45" stroke="#006600" stroke-width="8" fill="none">
<animate id="circle" attributeName="stroke-dasharray" from="0,282.743" to="282.743,0" dur="1s" begin="0s" fill="freeze"/>
<animateTransform attributeName="transform" type="rotate" from="-90 50 50" to="270 50 50" dur="1s" begin="0s" fill="freeze"/>
</circle>
<!-- 对勾动画 -->
<path d="M20 50 L40 70 L80 30" stroke="#006600" stroke-width="8" fill="none" visibility="hidden">
<set attributeName="visibility" from="hidden" to="visible" begin="circle.end+0.1s"/>
<!-- 对勾开始动画 -->
<!-- <animate attributeName="stroke-dasharray" from="0,120" to="120,0" dur="1s" begin="circle.end+0.1s" repeatCount="indefinite"/> -->
<animate id="check" attributeName="stroke-dasharray" from="0,120" to="120,0" dur="1s" begin="circle.end+0.1s" repeatCount="1"/>
<!-- 对勾隐藏动画 -->
<set attributeName="visibility" from="visible" to="hidden" begin="check.end+0.1s"/>
</path>
<!-- 扫码成功 -->
<!-- <text x="50" y="55" font-size="20" text-anchor="middle" fill="#006600" visibility="hidden">
扫码成功
<set attributeName="visibility" from="hidden" to="visible" begin="check.end+0.1s"/>
</text> -->
<text x="50" y="50" font-weight="bold" text-anchor="middle" fill="#006600" visibility="hidden">
<tspan x="50" dx="3" dy="-3" font-size="26">大门</tspan>
<tspan x="50" dy="23" font-size="20">已开启</tspan>
<set attributeName="visibility" from="hidden" to="visible" begin="check.end+0.1s"/>
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100" height="100" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle cx="50" cy="50" r="45" stroke="white" stroke-width="7" fill="white"/>
<!-- 外围圆圈 -->
<circle cx="50" cy="50" r="45" stroke="#006600" stroke-width="8" fill="none">
<animate id="circle" attributeName="stroke-dasharray" from="0,282.743" to="282.743,0" dur="1s" begin="0s" fill="freeze"/>
<animateTransform attributeName="transform" type="rotate" from="-90 50 50" to="270 50 50" dur="1s" begin="0s" fill="freeze"/>
</circle>
<!-- 对勾动画 -->
<path d="M20 50 L40 70 L80 30" stroke="#006600" stroke-width="8" fill="none" visibility="hidden">
<set attributeName="visibility" from="hidden" to="visible" begin="circle.end+0.1s"/>
<!-- 对勾开始动画 -->
<!-- <animate attributeName="stroke-dasharray" from="0,120" to="120,0" dur="1s" begin="circle.end+0.1s" repeatCount="indefinite"/> -->
<animate id="check" attributeName="stroke-dasharray" from="0,120" to="120,0" dur="1s" begin="circle.end+0.1s" repeatCount="1"/>
<!-- 对勾隐藏动画 -->
<set attributeName="visibility" from="visible" to="hidden" begin="check.end+0.1s"/>
</path>
<!-- 扫码成功 -->
<!-- <text x="50" y="55" font-size="20" text-anchor="middle" fill="#006600" visibility="hidden">
扫码成功
<set attributeName="visibility" from="hidden" to="visible" begin="check.end+0.1s"/>
</text> -->
<text x="50" y="50" font-size="27" font-weight="bold" text-anchor="middle" fill="#006600" visibility="hidden">
<tspan x="50" dx="4" dy="-5">扫码</tspan>
<tspan x="50" dy="28">成功</tspan>
<set attributeName="visibility" from="hidden" to="visible" begin="check.end+0.1s"/>
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -10,6 +10,7 @@
<title>社区疫情防控系统 - 门禁端</title>
<link rel="stylesheet" href="./assets/css/index.css"/>
<link rel="stylesheet" href="./assets/css/setting-panel.css"/>
<link rel="stylesheet" href="./assets/css/scan-result.css"/>
</head>
<body>
@@ -23,6 +24,9 @@
</div>
<div class="left-container">
<h1>进出社区请扫码</h1>
<p>1. 打开 微信 > 扫一扫,扫描右侧小程序码</p>
<p>2. 点击确认进入,门即开启</p>
<!--<h1>进出社区请扫码</h1>
<h3>社区居民</h3>
<p>1. 打开 微信 > 扫一扫,扫描右侧小程序码</p>
<p>2. 点击确认进入,门即开启</p>
@@ -32,15 +36,16 @@
<p>2. 填写进入申请表</p>
<h3>长期租客</h3>
<p>1. 请联系管理员为你添加进出权限</p>
<p>1. 请联系管理员为你添加进出权限</p>-->
</div>
<div class="right-container">
<h1 id="no-qrcode">请选择大门</h1>
<img id="qrcode" src="" style="display: none">
<img id="qrcode" src="" style="display: none;"><br>
<p id="refreshTimeCountDown"></p>
</div>
</div>
<!-- 设置页面 -->
<div id="full-screen-mask" class="full-screen-mask hidden"></div>
<div id="setting-container" class="setting-container hidden">
<div class="setting-panel">
@@ -63,8 +68,15 @@
</div>
</div>
</div>
<!-- 扫码成功 -->
<div id="scan-result-container" class="scan-result-container hidden">
<img id="qrcode-scan-success" class="scan-result-image" src="./assets/svg/scan-success.svg">
</div>
<script src="./assets/js/setting-panel.js" type="module"></script>
<script src="./renderer.js" type="module"></script>
<script src="assets/js/websocket-message-panel.js"></script>
<script src="./assets/js/websocket.js"></script>
</body>

View File

@@ -1,7 +1,18 @@
/**
* FIXME 环境配置
*
* baseUrl
* - 线上环境"https://epp.only4.work/"
* - 开发环境"/"
* envVersion
* - 线上环境"release"
* - 开发环境"develop"
*/
// 定义常量
const url = "https://epp-prod.only4.work/access/wechat/getUnlimitedQRCode"
const baseUrl = "/"
const url = baseUrl + "access/wechat/getUnlimitedQRCode"
const page = "pages/index/index" // "pages/scan/entrance"
const envVersion = "develop" // 正式版为 "release",体验版为 "trial",开发版为 "develop"
const envVersion = "release" // 正式版为 "release",体验版为 "trial",开发版为 "develop"
const autoColor = true
const isHyaline = false
const width = 500
@@ -102,7 +113,7 @@ window.changePanelSelectGate = changePanelSelectGate
// 发送请求,获取大门列表
async function getGateList() {
const response = await fetch('https://epp-prod.only4.work/access/gate/guard-client/getGateList');
const response = await fetch(baseUrl + 'access/gate/guard-client/getGateList');
const data = await response.json();
return data.data;
}

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基于微服务的社区疫情防控系统</title>
<style>
body {
font-size: 1.2em;
}
.grid-item {
border: 1px solid black;
text-align: center;
}
.grid-item ul {
text-align: left;
}
</style>
</head>
<body>
<div>
<h1 style="text-align: center;">基于微服务的社区疫情防控系统</h1>
<p style="text-align: center;">epp.only4.work</p>
<div style="display: grid; grid-template-columns: repeat(2, 1fr);">
<div class="grid-item">
<h3>
门禁端 跨端桌面应用
</h3>
<ul>
<li>
方式1访问
<a target="_blank" href="./guard/index.html">https://epp.only4.work/guard/index.html</a>
</li>
<li>
方式2运行跨端桌面应用
</li>
</ul>
</div>
<div class="grid-item">
<h3>
居民端 小程序
</h3>
<ul>
<li>
方式1扫描下方小程序码
<p style="text-align: center">
<img src="./assets/image/miniprogram.jpg" style="width: 180px;">
</p>
</li>
<li>
方式2微信搜索小程序 <b>devprogram</b>
</li>
</ul>
</div>
<div class="grid-item">
<h3>
社区管理员端 管理后台
</h3>
<ul>
<li>
访问
<a target="_blank" href="./manage/index.html">https://epp.only4.work/manage/index.html</a>
</li>
</ul>
</div>
<div class="grid-item">
<h3>
系统管理员端 管理后台
</h3>
<ul>
<li>
访问
<a target="_blank" href="./manage/index.html">https://epp.only4.work/manage/index.html</a>
</li>
</ul>
</div>
</div>
</div>
<div>
<h2>开发</h2>
<p>小程序后台:<a target="_blank">https://mp.weixin.qq.com/</a></p>
</div>
</body>
</html>

View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -0,0 +1 @@
.error-page[data-v-d4771405]{display:flex;justify-content:center;align-items:center;flex-direction:column;width:100%;height:100%;background:#f3f3f3;box-sizing:border-box}.error-code[data-v-d4771405]{line-height:1;font-size:250px;font-weight:bolder;color:#f02d2d}.error-code span[data-v-d4771405]{color:#00a854}.error-desc[data-v-d4771405]{font-size:30px;color:#777}.error-handle[data-v-d4771405]{margin-top:30px;padding-bottom:200px}.error-btn[data-v-d4771405]{margin-left:100px}

View File

@@ -0,0 +1 @@
.el-card{--el-card-border-color:var(--el-border-color-light);--el-card-border-radius:4px;--el-card-padding:20px;--el-card-bg-color:var(--el-fill-color-blank)}.el-card{border-radius:var(--el-card-border-radius);border:1px solid var(--el-card-border-color);background-color:var(--el-card-bg-color);overflow:hidden;color:var(--el-text-color-primary);transition:var(--el-transition-duration)}.el-card.is-always-shadow{box-shadow:var(--el-box-shadow-light)}.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{box-shadow:var(--el-box-shadow-light)}.el-card__header{padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding);border-bottom:1px solid var(--el-card-border-color);box-sizing:border-box}.el-card__body{padding:var(--el-card-padding)}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
:root{--el-popup-modal-bg-color:var(--el-color-black);--el-popup-modal-opacity:.5}.v-modal-enter{-webkit-animation:v-modal-in var(--el-transition-duration-fast) ease;animation:v-modal-in var(--el-transition-duration-fast) ease}.v-modal-leave{-webkit-animation:v-modal-out var(--el-transition-duration-fast) ease forwards;animation:v-modal-out var(--el-transition-duration-fast) ease forwards}@-webkit-keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{to{opacity:0}}@keyframes v-modal-out{to{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:var(--el-popup-modal-opacity);background:var(--el-popup-modal-bg-color)}.el-popup-parent--hidden{overflow:hidden}.el-dialog{--el-dialog-width:50%;--el-dialog-margin-top:15vh;--el-dialog-bg-color:var(--el-bg-color);--el-dialog-box-shadow:var(--el-box-shadow);--el-dialog-title-font-size:var(--el-font-size-large);--el-dialog-content-font-size:14px;--el-dialog-font-line-height:var(--el-font-line-height-primary);--el-dialog-padding-primary:20px;--el-dialog-border-radius:var(--el-border-radius-small);position:relative;margin:var(--el-dialog-margin-top,15vh) auto 50px;background:var(--el-dialog-bg-color);border-radius:var(--el-dialog-border-radius);box-shadow:var(--el-dialog-box-shadow);box-sizing:border-box;width:var(--el-dialog-width,50%)}.el-dialog:focus{outline:0!important}.el-dialog.is-align-center{margin:auto}.el-dialog.is-fullscreen{--el-dialog-width:100%;--el-dialog-margin-top:0;margin-bottom:0;height:100%;overflow:auto}.el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.el-dialog.is-draggable .el-dialog__header{cursor:move;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.el-dialog__header{padding:var(--el-dialog-padding-primary);padding-bottom:10px;margin-right:16px}.el-dialog__headerbtn{position:absolute;top:6px;right:0;padding:0;width:54px;height:54px;background:0 0;border:none;outline:0;cursor:pointer;font-size:var(--el-message-close-size,16px)}.el-dialog__headerbtn .el-dialog__close{color:var(--el-color-info);font-size:inherit}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:var(--el-color-primary)}.el-dialog__title{line-height:var(--el-dialog-font-line-height);font-size:var(--el-dialog-title-font-size);color:var(--el-text-color-primary)}.el-dialog__body{padding:calc(var(--el-dialog-padding-primary) + 10px) var(--el-dialog-padding-primary);color:var(--el-text-color-regular);font-size:var(--el-dialog-content-font-size)}.el-dialog__footer{padding:var(--el-dialog-padding-primary);padding-top:10px;text-align:right;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial;padding:25px calc(var(--el-dialog-padding-primary) + 5px) 30px}.el-dialog--center .el-dialog__footer{text-align:inherit}.el-overlay-dialog{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto}.dialog-fade-enter-active{-webkit-animation:modal-fade-in var(--el-transition-duration);animation:modal-fade-in var(--el-transition-duration)}.dialog-fade-enter-active .el-overlay-dialog{-webkit-animation:dialog-fade-in var(--el-transition-duration);animation:dialog-fade-in var(--el-transition-duration)}.dialog-fade-leave-active{-webkit-animation:modal-fade-out var(--el-transition-duration);animation:modal-fade-out var(--el-transition-duration)}.dialog-fade-leave-active .el-overlay-dialog{-webkit-animation:dialog-fade-out var(--el-transition-duration);animation:dialog-fade-out var(--el-transition-duration)}@-webkit-keyframes dialog-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}to{transform:translateZ(0);opacity:1}}@keyframes dialog-fade-in{0%{transform:translate3d(0,-20px,0);opacity:0}to{transform:translateZ(0);opacity:1}}@-webkit-keyframes dialog-fade-out{0%{transform:translateZ(0);opacity:1}to{transform:translate3d(0,-20px,0);opacity:0}}@keyframes dialog-fade-out{0%{transform:translateZ(0);opacity:1}to{transform:translate3d(0,-20px,0);opacity:0}}@-webkit-keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}@keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}@-webkit-keyframes modal-fade-out{0%{opacity:1}to{opacity:0}}@keyframes modal-fade-out{0%{opacity:1}to{opacity:0}}.el-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:2000;height:100%;background-color:var(--el-overlay-color-lighter);overflow:auto}.el-overlay .el-overlay-root{height:0}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.login-wrap[data-v-ae431c4c]{width:100%;height:100%}.login-container[data-v-ae431c4c]{width:100%;height:100%;display:grid;place-items:center}.ms-title[data-v-ae431c4c]{width:100%;padding:18px 24px;box-sizing:border-box;text-align:center;font-size:20px;color:#fff;border-bottom:1px solid #ddd}.ms-login[data-v-ae431c4c]{width:min(380px,95vw);padding:5px 10px;border-radius:5px;background:rgba(255,255,255,.3);overflow:hidden}.ms-content[data-v-ae431c4c]{padding:30px}.login-btn[data-v-ae431c4c]{text-align:center}.login-btn button[data-v-ae431c4c]{width:100%;height:36px;margin-bottom:10px}.company-info[data-v-ae431c4c]{color:#7589b6;text-align:center;position:absolute;left:0;right:0;bottom:10px;font-size:13px;letter-spacing:1px}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.line-height{line-height:2.7em}.row-index{width:110px;display:inline-block}

View File

@@ -0,0 +1,9 @@
/*!
* Cropper.js v1.5.13
* https://fengyuanchen.github.io/cropperjs
*
* Copyright 2015-present Chen Fengyuan
* Released under the MIT license
*
* Date: 2022-11-20T05:30:43.444Z
*/.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-wrap-box,.cropper-canvas,.cropper-drag-box,.cropper-crop-box,.cropper-modal{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-wrap-box,.cropper-canvas{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:calc(100% / 3);left:0;top:calc(100% / 3);width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:calc(100% / 3);top:0;width:calc(100% / 3)}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:before,.cropper-center:after{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width: 768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width: 992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width: 1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}.info[data-v-e7288b09]{text-align:center;padding:35px 0}.info-image[data-v-e7288b09]{position:relative;margin:auto;width:100px;height:100px;background:#f8f8f8;border:1px solid #eee;border-radius:50px;overflow:hidden}.info-edit[data-v-e7288b09]{display:flex;justify-content:center;align-items:center;position:absolute;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,.5);opacity:0;transition:opacity .3s ease}.info-edit i[data-v-e7288b09]{color:#eee;font-size:25px}.info-image:hover .info-edit[data-v-e7288b09]{opacity:1}.info-name[data-v-e7288b09]{margin:15px 0 10px;font-size:24px;font-weight:500;color:#262626}.crop-demo-btn[data-v-e7288b09]{position:relative}.crop-input[data-v-e7288b09]{position:absolute;width:100px;height:40px;left:0;top:0;opacity:0;cursor:pointer}

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title></title>
<link rel="stylesheet" href="https://at.alicdn.com/t/font_830376_qzecyukz0s.css">
<script type="module" crossorigin src="./assets/index.f92c5f31.js"></script>
<link rel="stylesheet" href="./assets/index.ca6f3a66.css">
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -61,6 +61,13 @@ public class AccessLogController {
return Res.error("参数错误");
}
// 推送到门禁端
JSONObject websocketData = new JSONObject();
websocketData.put("gateId", gateId);
websocketData.put("action", "onopen");
String websocketJSON = websocketData.toString();
WebSocketServer.sendAllMessage(websocketJSON);
AccessLog accessLog = new AccessLog();
accessLog.setId(null);
accessLog.setType(type);

View File

@@ -58,6 +58,14 @@ public class GateController {
Long gateId = Long.valueOf(id);
Gate gate = gateService.getGateById(gateId);
GateVO gateVO = GateVO.convertFrom(gate);
// 推送到门禁端
JSONObject websocketData = new JSONObject();
websocketData.put("gateId", id);
websocketData.put("action", "onscan");
String websocketJSON = websocketData.toString();
WebSocketServer.sendAllMessage(websocketJSON);
return Res.success(gateVO);
}

View File

@@ -1,5 +1,6 @@
package com.cxyxiaomo.epp.access.controller;
import com.alibaba.fastjson2.JSONObject;
import com.cxyxiaomo.epp.access.pojo.UnlimitedQRCodeParam;
import com.cxyxiaomo.epp.access.service.WeChatTokenServiceImpl;
import com.cxyxiaomo.epp.common.response.Res;
@@ -47,7 +48,28 @@ public class WeChatTokenController {
unlimitedQRCodeParam.setIsHyaline(isHyaline);
okhttp3.ResponseBody responseBody = weChatTokenService.getUnlimitedQRCodeFromApi(accessToken, unlimitedQRCodeParam);
return responseBody.bytes();
try {
// {"errcode":40001,"errmsg":"invalid credential, access_token is invalid or not latest, could get access_token by getStableAccessToken, more details at https://mmbizurl.cn/s/JtxxFh33r rid: 6446bac4-4a6b0410-118a16e7"}
if (Objects.requireNonNull(responseBody.contentType()).subtype().equals("json")) {
// 返回了 JSON 说明失败了
String jsonString = responseBody.string();
JSONObject jsonObject = JSONObject.parseObject(jsonString);
String errcode = jsonObject.getString("errcode");
if (errcode.equals("40001")) {
// 重新获取 Access Token
accessToken = weChatTokenService.getAccessToken(true);
responseBody = weChatTokenService.getUnlimitedQRCodeFromApi(accessToken, unlimitedQRCodeParam);
} else if (errcode.equals("40013")) {
System.out.println("40013 invalid appid 不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写\n" +
"docs: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html");
return null;
}
}
// 返回了图片,直接返回
return responseBody.bytes();
} catch (NullPointerException ignored) {
return null;
}
}
@GetMapping(value = "/rpc/getOpenIdFromApi")

View File

@@ -18,7 +18,7 @@ public class WebSocketServer {
private Session session;
// session集合,存放对应的session
private static ConcurrentHashMap<Integer, Session> sessionPool = new ConcurrentHashMap<>();
private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();
// concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
@@ -30,7 +30,7 @@ public class WebSocketServer {
* @param userId 用户ID
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") Integer userId) {
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
log.info("WebSocket建立连接中,连接用户ID{}", userId);
try {
Session historySession = sessionPool.get(userId);
@@ -84,7 +84,7 @@ public class WebSocketServer {
* @param userId 用户ID
* @param message 发送的消息
*/
public static void sendMessageByUser(Integer userId, String message) {
public static void sendMessageByUser(String userId, String message) {
log.info("用户ID" + userId + ",推送内容:" + message);
Session session = sessionPool.get(userId);
try {

View File

@@ -35,11 +35,17 @@ public class WeChatTokenServiceImpl implements WeChatTokenService {
@Override
public String getAccessToken() {
// 首先从数据库中查询是否存在 access_token
// 如果存在且没有过期,那么就直接返回(距离失效时间小于 3 分钟就当作过期)
Setting atSetting = accessDao.getValueByKey(SETTING_KEY);
if (atSetting != null && LocalDateTime.now().plusMinutes(3L).compareTo(atSetting.getTime()) < 0) {
return atSetting.getValue();
return getAccessToken(false);
}
public String getAccessToken(Boolean forceUpdate) {
if (!forceUpdate) {
// 首先从数据库中查询是否存在 access_token
// 如果存在且没有过期,那么就直接返回(距离失效时间小于 3 分钟就当作过期)
Setting atSetting = accessDao.getValueByKey(SETTING_KEY);
if (atSetting != null && LocalDateTime.now().plusMinutes(3L).compareTo(atSetting.getTime()) < 0) {
return atSetting.getValue();
}
}
// 否则则去请求一个新的 access_token

View File

@@ -56,7 +56,7 @@ public class GoodController {
searchText = null;
}
}
List<GoodVO> list = goodService.list(cateId, searchText);
List<GoodVO> list = goodService.listOnSale(cateId, searchText);
return Res.success(list);
}
@@ -100,7 +100,8 @@ public class GoodController {
// 分类列表
List<GoodCategory> cateList = goodService.getCateList();
String cateListForMock = JSONArray.from(cateList.stream().map(i -> i.getId()).collect(Collectors.toList())).toString();
String cateListForMock = JSONArray.from(cateList.stream()
.map(i -> i.getId().toString()).collect(Collectors.toList())).toString();
// id列 字段名区分大小写以VO中的变量名为准
// 新增、修改弹窗时,使用该列作为主键列进行操作
@@ -119,21 +120,30 @@ public class GoodController {
null // "IMG 120x120,随机图片"
)
.add("goodsName", "goodsName", "商品名称", "",
FieldType.TEXT, SearchType.INPUT, AddType.INPUT, EditType.CAN_NOT_EDIT,
FieldType.TEXT, SearchType.INPUT, AddType.INPUT, EditType.INPUT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
"商品名称", FieldBuilder.EDIT_PLACEHOLDER_SAME_AS_ADD_PLACEHOLDER,
FieldRuleListBuilder.create()
.add(FieldRuleBuilder.create("商品名称").required())
.add(FieldRuleBuilder.create("商品名称").minMax(6, 20)),
.add(FieldRuleBuilder.create("商品名称").minMax(2, 20)),
"DPD @cword(6, 20)"
)
.add("brief", "brief", "商品简介", "暂无简介",
FieldType.LONG_TEXT, SearchType.INPUT, AddType.INPUT, EditType.INPUT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
"商品简介", FieldBuilder.EDIT_PLACEHOLDER_SAME_AS_ADD_PLACEHOLDER,
FieldRuleListBuilder.create()
.add(FieldRuleBuilder.create("商品简介").required())
.add(FieldRuleBuilder.create("商品简介").minMax(2, 30)),
"DPD @cword(15, 30)"
)
.add("categoryId", "categoryName", "所属分类", "",
FieldType.TEXT, SearchType.SELECT, AddType.SELECT, EditType.SELECT,
"商品分类",
"商品分类", FieldBuilder.EDIT_PLACEHOLDER_SAME_AS_ADD_PLACEHOLDER,
FieldRuleListBuilder.create()
.add(FieldRuleBuilder.create("商品分类").required()),
"DPD @pick(" + cateListForMock + "])"
"DPD @pick(" + cateListForMock + ")"
)
.add("brand", "brand", "商品品牌", "",
FieldType.TEXT, SearchType.INPUT, AddType.INPUT, EditType.INPUT,
@@ -141,19 +151,10 @@ public class GoodController {
"商品品牌", FieldBuilder.EDIT_PLACEHOLDER_SAME_AS_ADD_PLACEHOLDER,
FieldRuleListBuilder.create()
.add(FieldRuleBuilder.create("商品品牌").required())
.add(FieldRuleBuilder.create("商品品牌").minMax(6, 20)),
.add(FieldRuleBuilder.create("商品品牌").minMax(2, 20)),
"DPD @cword(6, 15)"
)
.add("brief", "brief", "商品简介", "暂无简介",
FieldType.HIDDEN, SearchType.INPUT, AddType.INPUT, EditType.INPUT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
"商品简介", FieldBuilder.EDIT_PLACEHOLDER_SAME_AS_ADD_PLACEHOLDER,
FieldRuleListBuilder.create()
.add(FieldRuleBuilder.create("商品简介").required())
.add(FieldRuleBuilder.create("商品简介").minMax(6, 30)),
"DPD @cword(15, 30)"
)
.add("isOnSale", "isOnSale", "状态", true,
.add("isOnSale", "isOnSaleDisplay", "状态", true,
FieldType.TEXT, SearchType.SELECT, AddType.SELECT, EditType.SELECT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
"状态", FieldBuilder.EDIT_PLACEHOLDER_SAME_AS_ADD_PLACEHOLDER,
@@ -177,20 +178,24 @@ public class GoodController {
"DPD @cword(1, 2)"
)
.add("counterPrice", "counterPrice", "专柜价格", "",
FieldType.TEXT, SearchType.INPUT, AddType.INPUT, EditType.INPUT,
FieldType.TEXT, SearchType.INPUT, AddType.INPUT_NUMBER, EditType.INPUT_NUMBER,
"专柜价/原价/划线价",
"专柜价格 / 原价 / 划线价", FieldBuilder.EDIT_PLACEHOLDER_SAME_AS_ADD_PLACEHOLDER,
FieldRuleListBuilder.create()
.add(FieldRuleBuilder.create("专柜价格").required()),
"DTD /^\\d+?(\\.?\\d+?)?$/"
.add(FieldRuleBuilder.create("专柜价格").required())
// .add(FieldRuleBuilder.create("专柜价格").regexp("^(\\d+(\\.\\d{1,2})?)$"))
,
"DTD /^[1-9]\\d{0,4}(\\.?\\d{1,2})?$/"
)
.add("retailPrice", "retailPrice", "零售价格", "",
FieldType.TEXT, SearchType.INPUT, AddType.INPUT, EditType.INPUT,
FieldType.TEXT, SearchType.INPUT, AddType.INPUT_NUMBER, EditType.INPUT_NUMBER,
"零售价/售价/未划线价",
"零售价 / 售价 / 未划线价", FieldBuilder.EDIT_PLACEHOLDER_SAME_AS_ADD_PLACEHOLDER,
FieldRuleListBuilder.create()
.add(FieldRuleBuilder.create("零售价格").required()),
"DTD /^\\d+?(\\.?\\d+?)?$/"
.add(FieldRuleBuilder.create("零售价格").required())
// .add(FieldRuleBuilder.create("零售价格").regexp("^(\\d+(\\.\\d{1,2})?)$"))
,
"DTD /^[1-9]\\d{0,2}(\\.?\\d{1,2})?$/"
)
.add("detail", "detail", "商品详细介绍", "暂无商品详细介绍",
FieldType.LONG_TEXT, SearchType.INPUT, AddType.TEXTAREA, EditType.TEXTAREA,
@@ -212,7 +217,7 @@ public class GoodController {
// build
JSONArray fieldMapper = FieldMapperBuilder.create()
.add("categoryId", "categoryName", cateMap)
.add("isOnSale", "isOnSale", stateMap)
.add("isOnSale", "isOnSaleDisplay", stateMap)
.build();
// 拼装返回结果
@@ -237,6 +242,18 @@ public class GoodController {
@ResponseBody
public Res editGood(@ModelAttribute GoodVO goodVO) {
Good good = GoodVO.convertTo(goodVO);
Double retailPrice = good.getRetailPrice();
if (Objects.nonNull(retailPrice)) {
if (retailPrice < 0 || retailPrice > 99999999.99) {
return Res.error("零售价格输入不符,须在 0 - 99999999.99 之间");
}
}
Double counterPrice = good.getCounterPrice();
if (Objects.nonNull(counterPrice)) {
if (counterPrice < 0 || counterPrice > 99999999.99) {
return Res.error("专柜价格输入不符,须在 0 - 99999999.99 之间");
}
}
// 先查询商品是否存在
Good existGood = goodService.getGoodById(good.getId());

View File

@@ -2,18 +2,31 @@ package com.cxyxiaomo.epp.shop.controller;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.cxyxiaomo.epp.PageTable.enums.AddType;
import com.cxyxiaomo.epp.PageTable.enums.EditType;
import com.cxyxiaomo.epp.PageTable.enums.FieldType;
import com.cxyxiaomo.epp.PageTable.enums.SearchType;
import com.cxyxiaomo.epp.PageTable.query.PageQuery;
import com.cxyxiaomo.epp.PageTable.utils.FieldBuilder;
import com.cxyxiaomo.epp.PageTable.utils.FieldMapperBuilder;
import com.cxyxiaomo.epp.PageTable.utils.FieldRuleBuilder;
import com.cxyxiaomo.epp.PageTable.utils.FieldRuleListBuilder;
import com.cxyxiaomo.epp.common.enums.OrderStatus;
import com.cxyxiaomo.epp.common.pojo.Order;
import com.cxyxiaomo.epp.common.pojo.OrderDetail;
import com.cxyxiaomo.epp.common.query.OrderQuery;
import com.cxyxiaomo.epp.common.response.Res;
import com.cxyxiaomo.epp.common.vo.GoodVO;
import com.cxyxiaomo.epp.common.vo.OrderDetailVO;
import com.cxyxiaomo.epp.common.vo.OrderVO;
import com.cxyxiaomo.epp.shop.service.GoodService;
import com.cxyxiaomo.epp.shop.service.OrderService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@@ -30,8 +43,8 @@ public class OrderController {
/**
* 小程序端创建订单
*
* @param userId 下单用户
* @param orderList 下单商品 [ { goodId, count }, ... ]
* @param params userId 下单用户
* @param params orderList 下单商品 [ { goodId, count }, ... ]
* @return
*/
@PostMapping("/miniprogram/createOrder")
@@ -184,6 +197,12 @@ public class OrderController {
return Res.success(success ? "支付成功" : "支付失败");
}
/**
* 用户取消订单
*
* @param params
* @return
*/
@PostMapping("/miniprogram/cancelOrder")
@ResponseBody
public Res cancelOrder(@RequestBody JSONObject params) {
@@ -224,6 +243,106 @@ public class OrderController {
return Res.success(success ? "取消成功" : "取消失败");
}
/**
* 订单发货
*
* @param orderId
* @param expressId
* @param comment
* @return
*/
@PostMapping("/manage/deliverOrder")
@ResponseBody
public Res deliverOrder(Long orderId, String expressId, String comment) {
if (orderId == null || expressId == null || comment == null) {
return Res.error("参数错误");
}
if (expressId.equals("") && comment.equals("")) {
return Res.error("运单号 和 发货备注 不可同时为空");
}
// 查询订单详情
Order order = orderService.getOrderById(orderId);
if (order == null) {
return Res.error("订单不存在");
}
//判断是否可以发货
OrderStatus orderStatus = OrderStatus.get(order.getOrderStatus());
switch (orderStatus) {
case PENDING:
return Res.error("订单尚未支付,不可发货");
case PROCESSING: // 订单未发货
// 更新订单发货信息
orderService.updateOrderShipInfo(orderId, expressId, comment);
// 更新支付信息
orderService.updateOrderStatus(orderId, OrderStatus.SHIPPED);
return Res.success("发货成功");
case SHIPPED: // 订单已发货,修改发货信息
// 更新订单发货信息
orderService.updateOrderShipInfo(orderId, expressId, comment);
return Res.success("发货信息修改成功");
case DELIVERED:
return Res.error("订单已完成,无法再次发货");
case CANCELLED:
return Res.error("订单已取消,不可发货");
default:
return Res.error("当前订单状态不可发货");
}
}
/**
* 管理员取消订单
*
* @param orderId
* @return
*/
@PostMapping("/manage/cancelOrder")
@ResponseBody
public Res cancelOrderByManager(Long orderId) {
if (orderId == null) {
return Res.error("参数错误");
}
// 查询订单详情
Order order = orderService.getOrderById(orderId);
if (order == null) {
return Res.error("订单不存在");
}
//判断是否可以取消
OrderStatus orderStatus = OrderStatus.get(order.getOrderStatus());
switch (orderStatus) {
case PENDING:
// 更新订单发货信息
orderService.updateOrderShipInfo(orderId, "", "管理员取消了您的订单,如有疑问请联系管理员");
// 更新支付信息
orderService.updateOrderStatus(orderId, OrderStatus.CANCELLED);
return Res.success("操作成功");
case PROCESSING:
// 更新订单发货信息
orderService.updateOrderShipInfo(orderId, "", "管理员取消了您的订单,费用已退回至您的帐户,如有疑问请联系管理员");
// 更新支付信息
orderService.updateOrderStatus(orderId, OrderStatus.CANCELLED);
return Res.success("操作成功");
case SHIPPED:
return Res.error("订单已发货,不可取消");
case DELIVERED:
return Res.error("订单已送达,不可取消");
case CANCELLED:
return Res.error("订单已取消");
default:
return Res.error("当前订单状态无法取消");
}
}
/**
* 订单确认收货
*
* @param params
* @return
*/
@PostMapping("/miniprogram/confirmOrder")
@ResponseBody
public Res confirmOrder(@RequestBody JSONObject params) {
@@ -261,4 +380,189 @@ public class OrderController {
return Res.success(success ? "确认收货成功" : "确认收货失败");
}
/**
* 获取订单列表
*
* @return
*/
@GetMapping("/manage/getOrderList")
@ResponseBody
public Res getOrderList(PageQuery pageQuery, OrderQuery orderQuery) {
// 查询分页数据
PageHelper.startPage(pageQuery.getPageIndex(), pageQuery.getPageSize());
List<Order> orderList = orderService.getOrderList(orderQuery);
PageInfo<Order> orderPageInfo = new PageInfo<>(orderList);
List<Order> list = orderPageInfo.getList();
List<OrderVO> voList = OrderVO.convertFrom(list);
// id列 字段名区分大小写以VO中的变量名为准
// 新增、修改弹窗时,使用该列作为主键列进行操作
String idFieldName = "id";
// 当前管理页面
String pageName = "订单管理";
// 指定前端表格显示列
JSONArray columns = FieldBuilder.create()
.add("userId", "userId", "用户ID", "",
FieldType.TEXT, SearchType.INPUT, AddType.CAN_NOT_ADD, EditType.CAN_NOT_EDIT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, null, null, null
)
.add("orderStatusCode", "orderStatusName", "订单状态", true,
FieldType.TEXT, SearchType.SELECT, AddType.CAN_NOT_ADD, EditType.SELECT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, "订单状态",
FieldRuleListBuilder.create()
.add(FieldRuleBuilder.create("订单状态").required()),
null
)
.add("orderPrice", "orderPrice", "订单价格", "",
FieldType.TEXT, SearchType.CAN_NOT_SEARCH, AddType.CAN_NOT_ADD, EditType.CAN_NOT_EDIT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, null, null, null
)
.add("orderDate", "orderDate", "下单时间", "",
FieldType.TEXT, SearchType.CAN_NOT_SEARCH, AddType.CAN_NOT_ADD, EditType.CAN_NOT_EDIT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, null, null, null
)
.add("payDate", "payDate", "支付时间", "",
FieldType.TEXT, SearchType.CAN_NOT_SEARCH, AddType.CAN_NOT_ADD, EditType.CAN_NOT_EDIT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, null, null, null
)
.add("cancelDate", "cancelDate", "取消时间", "",
FieldType.TEXT, SearchType.CAN_NOT_SEARCH, AddType.CAN_NOT_ADD, EditType.CAN_NOT_EDIT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, null, null, null
)
.add("shipDate", "shipDate", "发货时间", "",
FieldType.TEXT, SearchType.CAN_NOT_SEARCH, AddType.CAN_NOT_ADD, EditType.CAN_NOT_EDIT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, null, null, null
)
.add("deliverDate", "deliverDate", "送达时间", "",
FieldType.TEXT, SearchType.CAN_NOT_SEARCH, AddType.CAN_NOT_ADD, EditType.CAN_NOT_EDIT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, null, null, null
)
.add("expressId", "expressId", "运单号", "",
FieldType.TEXT, SearchType.INPUT, AddType.CAN_NOT_ADD, EditType.CAN_NOT_EDIT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, null, null, null
)
.add("comment", "comment", "发货备注", "",
FieldType.LONG_TEXT, SearchType.CAN_NOT_SEARCH, AddType.CAN_NOT_ADD, EditType.CAN_NOT_EDIT,
FieldBuilder.SEARCH_PLACEHOLDER_SAME_AS_FIELDNAME,
null, null, null, null
)
.build();
// 指定需要翻译的字段
OrderStatus[] orderStatusList = OrderStatus.values();
HashMap<String, String> orderStatusMap = new HashMap<>(orderStatusList.length);
for (OrderStatus orderStatus : orderStatusList) {
orderStatusMap.put(orderStatus.getValue(), orderStatus.toString());
}
// build
JSONArray fieldMapper = FieldMapperBuilder.create()
.add("orderStatusCode", "orderStatusName", orderStatusMap)
.build();
// 拼装返回结果
JSONObject map = new JSONObject(6);
map.put("total", orderPageInfo.getTotal());
map.put("list", voList);
map.put("columns", columns);
map.put("fieldMapper", fieldMapper);
map.put("idFieldName", idFieldName);
map.put("pageName", pageName);
// 返回结果
return Res.success(map);
}
// /**
// * 编辑订单
// *
// * @return
// */
// @PostMapping("/manage/editOrder")
// @ResponseBody
// public Res editOrder(@ModelAttribute OrderVO OrderVO) {
// Order Order = OrderVO.convertTo(OrderVO);
//
// // 先查询订单是否存在
// Order existOrder = orderService.getOrderById(Order.getId());
//
// if (Order.getId() == null || Order.getId() < 1) {
// // 新增订单
// if (existOrder != null) {
// return Res.error("订单已存在,操作失败");
// }
//
// if (Order.getOrdersName() == null || "".equals(Order.getOrdersName())) {
// return Res.error("订单名称不能为空");
// }
// Order.setId(null);
// orderService.addOrder(Order);
// } else {
// // 修改订单
// if (existOrder == null) {
// return Res.error("订单不存在,操作失败");
// }
//
// orderService.updateOrder(Order);
// }
// return Res.success(true);
// }
//
// /**
// * 关闭订单
// *
// * @param id
// * @return
// */
// @PostMapping("/manage/deleteOrder")
// @ResponseBody
// public Res deleteOrder(Long id) {
// if (id == null || id <= 0) {
// return Res.error("订单不存在,删除失败");
// }
// // 先查询订单是否存在
// Order existOrder = orderService.getOrderById(id);
// if (existOrder == null) {
// return Res.error("订单不存在,删除失败");
// }
// boolean b = orderService.deleteOrder(existOrder.getId());
// return Res.success(b);
// }
/**
* 导出订单列表
*
* @return
*/
@GetMapping("/manage/exportOrderList")
@ResponseBody
public Res exportOrderList(OrderQuery orderQuery) {
List<Order> orderList = orderService.getOrderList(orderQuery);
List<OrderVO> orderVOList = OrderVO.convertFrom(orderList);
// 当前时间
Date now = Calendar.getInstance().getTime();
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd_HHmmss");
String dateTime = format.format(now);
HashMap<String, Object> map = new HashMap<>();
map.put("list", orderVOList);
map.put("sheetName", "订单表-" + System.currentTimeMillis());
map.put("fileName", "订单表_导出时间_" + dateTime);
return Res.success(map);
}
}

View File

@@ -14,6 +14,8 @@ public interface GoodDao {
List<Good> list(@Param("cateId") Integer cateId, @Param("searchText") String searchText);
List<Good> listOnSale(@Param("cateId") Integer cateId, @Param("searchText") String searchText);
Good getById(Long id);
Good selectById(Long id);
@@ -27,13 +29,7 @@ public interface GoodDao {
Integer deleteById(Integer id);
// Manage
public boolean addGood(Good good);
public boolean updateGood(Good good);

View File

@@ -2,6 +2,7 @@ package com.cxyxiaomo.epp.shop.dao;
import com.cxyxiaomo.epp.common.pojo.Order;
import com.cxyxiaomo.epp.common.pojo.OrderDetail;
import com.cxyxiaomo.epp.common.query.OrderQuery;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@@ -62,6 +63,19 @@ public interface OrderDao {
*/
int updateOrderStatus(@Param("orderId") Long orderId, @Param("orderStatus") String orderStatus);
/**
* 更新订单发货信息
*
* @param orderId
* @param expressId
* @param comment
* @return
*/
int updateOrderShipInfo(@Param("orderId") Long orderId, @Param("expressId") String expressId, @Param("comment") String comment);
// 根据订单 ID 删除订单信息及订单详情信息
int deleteOrderById(Long orderId);
boolean deleteOrderById(Long orderId);
public List<Order> getOrderList(OrderQuery orderQuery);
}

View File

@@ -27,6 +27,13 @@ public class GoodService {
return goodVOS;
}
// 只列出上架状态的商品
public List<GoodVO> listOnSale(Integer cateId, String searchText) {
List<Good> list = goodDao.listOnSale(cateId, searchText);
List<GoodVO> goodVOS = GoodVO.convertFrom(list);
return goodVOS;
}
public GoodVO getById(Long id) {
Good good = goodDao.getById(id);
GoodVO goodVO = GoodVO.convertFrom(good);

View File

@@ -4,6 +4,7 @@ import com.cxyxiaomo.epp.common.enums.OrderStatus;
import com.cxyxiaomo.epp.common.pojo.Good;
import com.cxyxiaomo.epp.common.pojo.Order;
import com.cxyxiaomo.epp.common.pojo.OrderDetail;
import com.cxyxiaomo.epp.common.query.OrderQuery;
import com.cxyxiaomo.epp.common.utils.SnowflakeManager;
import com.cxyxiaomo.epp.common.vo.OrderDetailVO;
import com.cxyxiaomo.epp.shop.dao.GoodDao;
@@ -132,4 +133,15 @@ public class OrderService {
int affectRows = orderDao.updateOrderStatus(orderId, orderStatus.getValue());
return affectRows > 0;
}
public Boolean updateOrderShipInfo(Long orderId, String expressId, String comment) {
int affectRows = orderDao.updateOrderShipInfo(orderId, expressId, comment);
return affectRows > 0;
}
public List<Order> getOrderList(OrderQuery orderQuery) {
return orderDao.getOrderList(orderQuery);
}
}

View File

@@ -37,6 +37,20 @@
</if>
order by `sort_order` asc
</select>
<select id="listOnSale" resultType="com.cxyxiaomo.epp.common.pojo.Good">
SELECT *
FROM goods
WHERE is_on_sale = 1
<if test="cateId != null">
AND category_id = #{cateId}
</if>
<if test="searchText != null">
AND
( goods_name LIKE concat('%',#{searchText,jdbcType=VARCHAR},'%')
OR brief LIKE concat('%',#{searchText,jdbcType=VARCHAR},'%'))
</if>
order by `sort_order` asc
</select>
<select id="getById" parameterType="java.lang.Long" resultType="com.cxyxiaomo.epp.common.pojo.Good">
SELECT *
FROM goods

View File

@@ -95,7 +95,7 @@
</update>
<!-- 根据订单 ID 删除订单信息及订单详情信息 -->
<delete id="deleteOrderById" parameterType="java.lang.Integer">
<delete id="deleteOrderById" parameterType="java.lang.Long">
DELETE
FROM `order`
WHERE id = #{orderId};
@@ -104,4 +104,39 @@
-- FROM goods_order_details
-- WHERE order_id = #{orderId};
</delete>
<!-- manage 相关的 SQL -->
<select id="getOrderList" resultType="com.cxyxiaomo.epp.common.pojo.Order"
parameterType="com.cxyxiaomo.epp.common.query.OrderQuery">
SELECT * FROM `order`
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="orderStatusCode != null and orderStatusCode != ''">
AND order_status = #{orderStatusCode}
</if>
<if test="expressId != null and expressId != ''">
AND express_id = #{expressId}
</if>
</where>
</select>
<!-- 更新发货信息 -->
<update id="updateOrderShipInfo" parameterType="com.cxyxiaomo.epp.common.pojo.Order">
UPDATE `order`
<set>
<if test="expressId != null">
express_id = #{expressId},
</if>
<if test="comment != null">
`comment` = #{comment},
</if>
</set>
WHERE id = #{orderId}
</update>
</mapper>

View File

@@ -11,7 +11,14 @@
</head>
<body>
<iframe src="https://epp-prod.only4.work/access/index.html?inElectron=true" frameborder="0"></iframe>
<!--
FIXME 环境配置
src
- 线上环境:"https://epp.only4.work/guard/index.html?inElectron=true"
- 开发环境:"http://localhost/guard/index.html?inElectron=true"
-->
<iframe src="https://epp.only4.work/guard/index.html?inElectron=true" frameborder="0"></iframe>
<script src="./renderer.js" type="module"></script>
</body>

File diff suppressed because it is too large Load Diff

3
database/备注.md Normal file
View File

@@ -0,0 +1,3 @@
该SQL中包含测试数据若不需要可以自行删除
数据表名最后带 1 的为项目未使用到的表,可以直接删除

View File

@@ -7,13 +7,10 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
Calender: typeof import('./src/components/calender.vue')['default']
ContextMenu: typeof import('./src/components/context-menu.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -30,7 +27,6 @@ declare module '@vue/runtime-core' {
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
@@ -38,6 +34,7 @@ declare module '@vue/runtime-core' {
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
Header: typeof import('./src/components/header.vue')['default']

View File

@@ -24,6 +24,7 @@
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/mockjs": "^1.0.7",
"@vitejs/plugin-vue": "^3.0.0",
"@vue/compiler-sfc": "^3.1.2",
"typescript": "^4.6.4",
@@ -223,6 +224,12 @@
"@types/lodash": "*"
}
},
"node_modules/@types/mockjs": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/@types/mockjs/-/mockjs-1.0.7.tgz",
"integrity": "sha512-OCxXz6hEaJOVpRwuJMiVY5a6LtJcih+br9gwB/Q8ooOBikvk5FpBQ31OlNimXo3EqKha1Z7PFBni+q9m+8NCWg==",
"dev": true
},
"node_modules/@types/node": {
"version": "18.14.0",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-18.14.0.tgz",

View File

@@ -24,6 +24,7 @@
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/mockjs": "^1.0.7",
"@vitejs/plugin-vue": "^3.0.0",
"@vue/compiler-sfc": "^3.1.2",
"typescript": "^4.6.4",

View File

@@ -0,0 +1,63 @@
import send_request from '../utils/send_request';
/**
* 获取订单列表
* @returns
*/
export function getOrderList(params) {
return send_request({
url: '/shop/order/manage/getOrderList',
method: 'GET',
params: params,
});
};
/**
* 获取订单详情
* @returns
*/
export function getOrderDetail(params) {
return send_request({
url: '/shop/order/miniprogram/orderDetail',
method: 'GET',
params: params,
});
};
/**
* 订单发货
* @returns
*/
export function deliverOrder(params) {
return send_request({
url: '/shop/order/manage/deliverOrder',
method: 'POST',
useQS: true,
params: params,
});
};
/**
* 关闭订单
* @returns
*/
export function withdrawOrder(params) {
return send_request({
url: '/shop/order/manage/cancelOrder',
method: 'POST',
useQS: true,
params: params,
});
};
/**
* 导出订单列表
* @returns
*/
export function exportOrderList(params) {
return send_request({
url: '/shop/order/manage/exportOrderList',
method: 'GET',
params: params,
});
};

View File

@@ -24,6 +24,24 @@ export function userLogout() {
return null;
};
/**
* 修改密码
* @returns
*/
export function updatePwd({ oldpwd, newpwd }) {
let userId = localStorage.getItem("ms_user_id")
return send_request({
url: '/user/updatePwd',
method: 'POST',
useQS: true,
params: {
"userId": userId,
"oldpwd": oldpwd,
"newpwd": newpwd,
},
});
};
/**
* 获取用户列表
* @returns

View File

@@ -13,7 +13,7 @@
<div class="header-right">
<div class="header-user-con">
<!-- 消息中心 -->
<div class="btn-bell" @click="router.push('/tabs')">
<div class="btn-bell" @click="router.push('/tabs')" v-if="false">
<el-tooltip effect="dark" :content="message ? `有${message}条未读消息` : `消息中心`" placement="bottom">
<i class="el-icon-lx-notice"></i>
</el-tooltip>

View File

@@ -5,9 +5,9 @@
<!-- 筛选 -->
<div class="handle-box">
<template v-for="field in searchFields">
<el-input v-if="field.searchType == 'input'" v-model="query[field.field]"
@keyup.enter.native="handleSearch" :placeholder="field.placeholder" :prefix-icon="Filter"
class="handle-input mr10"></el-input>
<el-input v-if="field.searchType == 'input' || field.searchType == 'input-number'"
v-model="query[field.field]" @keyup.enter.native="handleSearch" :placeholder="field.placeholder"
:prefix-icon="Filter" class="handle-input mr10"></el-input>
<el-select v-else-if="field.searchType == 'select'" v-model="query[field.field]" :clearable="true"
@change="handleSearch" :placeholder="field.placeholder" class="handle-select mr10">
<template #prefix>
@@ -56,16 +56,27 @@
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" v-if="props.editFunc || props.deleteFunc">
<el-table-column label="操作" width="220" align="center"
v-if="props.editFunc || props.deleteFunc || props.customEditHandle">
<template #default="scope">
<el-button text :icon="Edit" @click="handleEdit(scope.$index, scope.row)"
v-permiss="props.editPermiss" v-if="props.editFunc">
编辑
</el-button>
<el-button text :icon="Delete" class="red" @click="handleDelete(scope.$index, scope.row)"
v-permiss="props.editPermiss" v-if="props.deleteFunc">
删除
</el-button>
<template v-if="props.customEditHandle">
<el-button text :icon="List"
@click="props.customEditHandle?.((scope as any).$index, (scope as any).row, getData)"
v-permiss="props.editPermiss">
管理
</el-button>
</template>
<template v-else>
<el-button text :icon="Edit" @click="handleEdit((scope as any).$index, (scope as any).row)"
v-permiss="props.editPermiss" v-if="props.editFunc">
编辑
</el-button>
<el-button text :icon="Delete" class="red"
@click="handleDelete((scope as any).$index, (scope as any).row)"
v-permiss="props.editPermiss" v-if="props.deleteFunc">
删除
</el-button>
</template>
</template>
</el-table-column>
</el-table>
@@ -87,6 +98,9 @@
<el-input v-if="(formId > 0 ? field.editType : field.addType) == 'input'"
:placeholder="formId > 0 ? field.editPlaceholder : field.addPlaceholder" class="popup-item"
v-model="form[field.field]"></el-input>
<el-input v-else-if="(formId > 0 ? field.editType : field.addType) == 'input-number'" type="number"
:placeholder="formId > 0 ? field.editPlaceholder : field.addPlaceholder" class="popup-item"
v-model="form[field.field]"></el-input>
<el-input v-else-if="(formId > 0 ? field.editType : field.addType) == 'textarea'"
:placeholder="formId > 0 ? field.editPlaceholder : field.addPlaceholder" class="popup-item"
v-model="form[field.field]" type="textarea" :rows="4"></el-input>
@@ -141,7 +155,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { FormInstance, FormRules, ElMessage, ElMessageBox } from 'element-plus';
import { Delete, Edit, Search, Plus, Filter, Download } from '@element-plus/icons-vue';
import { Delete, Edit, Search, Plus, Filter, Download, List } from '@element-plus/icons-vue';
import * as xlsx from 'xlsx';
import Mock from 'mockjs';
import ImageUpload from './image-upload.vue';
@@ -176,6 +190,12 @@ const props = defineProps({
'editPermiss': {
type: String,
required: true,
},
// 自定义修改按钮点击事件
'customEditHandle': {
type: Function,
required: false,
}
})
@@ -379,7 +399,8 @@ const handleEdit = (index: number, row: any) => {
for (let f of dialogFields.value) {
switch (f.editType) {
case "select":
// 下拉框的值要为 string 类型
case "input-number":
// 下拉框、数字输入框的值要为 string 类型
form[f.field] = String(row[f.field]);
break;
default:
@@ -399,7 +420,8 @@ const handleNew = () => {
for (let f of dialogFields.value) {
switch (f.editType) {
case "select":
// 下拉框的值要为 string 类型
case "input-number":
// 下拉框、数字输入框的值要为 string 类型
form[f.field] = String(f.default);
break;
default:
@@ -431,10 +453,10 @@ const saveEdit = async (formEl: FormInstance | undefined) => {
var result;
if (formId > 0) {
// 修改记录
var result = await props.editFunc(form)
var result = await props.editFunc?.(form)
} else {
// 新增记录
var result = await props.addFunc(form)
var result = await props.addFunc?.(form)
query.pageIndex = Math.ceil((pageTotal.value + 1) / query.pageSize);
}
console.log("result", result)
@@ -460,7 +482,7 @@ const handleDelete = (index: number, row: any) => {
// 二次确认删除
ElMessageBox.confirm('确定要删除吗?', '提示', { type: 'warning' })
.then(async () => {
var result = await props.deleteFunc({
var result = await props.deleteFunc?.({
id: row[idFieldName],
})
if (result) {
@@ -540,7 +562,7 @@ const handleExport = async () => {
// 数据部分
let excelList = dataList.map((row: any) => {
// 通过翻译前的 key 拿数据
return fieldNameList.map((field: any) => String(row[field]))
return fieldNameList.map((field: any) => String(row[field] || ""))
})
excelList.unshift(firstRow) // 插入表头

View File

@@ -91,6 +91,11 @@ const items = [
title: '商品管理',
permiss: 'shop-good-setting',
},
{
index: '/shop-order-setting',
title: '订单管理',
permiss: 'shop-order-setting',
},
],
},
{

View File

@@ -84,6 +84,15 @@ const routes: RouteRecordRaw[] = [
},
component: () => import('../views/shop-good-setting.vue'),
},
{
path: '/shop-order-setting',
name: 'shop-order-setting',
meta: {
title: '订单管理',
permiss: 'shop-order-setting',
},
component: () => import('../views/shop-order-setting.vue'),
},
{

View File

@@ -15,16 +15,17 @@ export const usePermissStore = defineStore('permiss', {
"dashboard",
"access",
"access-log",
"access-gate-setting",
// "access",
// "access-log",
// "access-gate-setting",
"report",
"report-log",
// "report",
// "report-log",
"shop",
"shop-cate-setting",
"shop-good-setting",
// "shop",
// "shop-cate-setting",
// "shop-good-setting",
// "shop-order-setting",
"privilege",
"privilege-user-setting",
@@ -46,9 +47,10 @@ export const usePermissStore = defineStore('permiss', {
"shop",
"shop-cate-setting",
"shop-good-setting",
"shop-order-setting",
"privilege",
"privilege-user-setting",
// "privilege",
// "privilege-user-setting",
]
};
},

View File

@@ -22,6 +22,12 @@ export default {
/**
* 后端接口请求地址
* (结尾加不加 / 都可)
*
* FIXME 环境配置
*
* backendHost
* - 线上环境:"https://epp.only4.work/"
* - 开发环境:"http://localhost/"
*/
backendHost: "https://epp-prod.only4.work/",
backendHost: "http://localhost/",
};

View File

@@ -1,213 +1,26 @@
<template>
<div class="container">
<el-row :gutter="20">
<el-col :span="8">
<el-card shadow="hover" class="mgb20" style="height: 252px">
<div class="user-info">
<el-avatar :size="120" :src="imgurl" />
<div class="user-info-cont">
<div class="user-info-name">{{ name }}</div>
<div>{{ role }}</div>
</div>
</div>
<div class="user-info-list">
上次登录时间
<span>2022-10-01</span>
</div>
<div class="user-info-list">
上次登录地点
<span>东莞</span>
</div>
</el-card>
<el-card shadow="hover" style="height: 252px">
<template #header>
<div class="clearfix">
<span>语言详情</span>
</div>
</template>
Vue
<el-progress :percentage="79.4" color="#42b983"></el-progress>
TypeScript
<el-progress :percentage="14" color="#f1e05a"></el-progress>
CSS
<el-progress :percentage="5.6"></el-progress>
HTML
<el-progress :percentage="1" color="#f56c6c"></el-progress>
</el-card>
</el-col>
<el-col :span="16">
<el-row :gutter="20" class="mgb20">
<el-col :span="8">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-1">
<el-icon class="grid-con-icon"><User /></el-icon>
<div class="grid-cont-right">
<div class="grid-num">1234</div>
<div>用户访问量</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-2">
<el-icon class="grid-con-icon"><ChatDotRound /></el-icon>
<div class="grid-cont-right">
<div class="grid-num">321</div>
<div>系统消息</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-3">
<el-icon class="grid-con-icon"><Goods /></el-icon>
<div class="grid-cont-right">
<div class="grid-num">500</div>
<div>商品数量</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card shadow="hover" style="height: 403px">
<template #header>
<div class="clearfix">
<span>预警列表</span>
<el-button style="float: right; padding: 3px 0" text>添加</el-button>
</div>
</template>
<el-table :show-header="false" :data="todoList" style="width: 100%">
<el-table-column width="40">
<template #default="scope">
<el-checkbox v-model="scope.row.status"></el-checkbox>
</template>
</el-table-column>
<el-table-column>
<template #default="scope">
<div
class="todo-item"
:class="{
'todo-item-del': scope.row.status
}"
>
{{ scope.row.title }}
</div>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
<div class="container">
<el-card shadow="hover" class="mgb20" style="height: 252px">
<div class="user-info">
<el-avatar :size="120" :src="imgurl" />
<div class="user-info-cont">
<div class="user-info-name">{{ name }}</div>
<div>{{ role }}</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts" name="dashboard">
import { ref, reactive } from 'vue';
import imgurl from '../assets/img/img.jpg';
const roleMap = {
}
const name = localStorage.getItem('ms_username');
const role: string = name === 'admin' ? '超级管理员' : '普通用户';
</script>
<style scoped>
.el-row {
margin-bottom: 20px;
}
.grid-content {
display: flex;
align-items: center;
height: 100px;
}
.grid-cont-right {
flex: 1;
text-align: center;
font-size: 14px;
color: #999;
}
.grid-num {
font-size: 30px;
font-weight: bold;
}
.grid-con-icon {
font-size: 50px;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
color: #fff;
}
.grid-con-1 .grid-con-icon {
background: rgb(45, 140, 240);
}
.grid-con-1 .grid-num {
color: rgb(45, 140, 240);
}
.grid-con-2 .grid-con-icon {
background: rgb(100, 213, 114);
}
.grid-con-2 .grid-num {
color: rgb(100, 213, 114);
}
.grid-con-3 .grid-con-icon {
background: rgb(242, 94, 67);
}
.grid-con-3 .grid-num {
color: rgb(242, 94, 67);
}
.user-info {
display: flex;
align-items: center;
padding-bottom: 20px;
border-bottom: 2px solid #ccc;
margin-bottom: 20px;
}
.user-info-cont {
padding-left: 50px;
flex: 1;
font-size: 14px;
color: #999;
}
.user-info-cont div:first-child {
font-size: 30px;
color: #222;
}
.user-info-list {
font-size: 14px;
color: #999;
line-height: 25px;
}
.user-info-list span {
margin-left: 70px;
}
.mgb20 {
margin-bottom: 20px;
}
.todo-item {
font-size: 14px;
}
.todo-item-del {
text-decoration: line-through;
color: #999;
}
</style>
<style scoped></style>

View File

@@ -67,8 +67,8 @@ interface RoleInfo {
const router = useRouter();
const param = reactive<LoginInfo>({
username: 'root',
password: 'root'
username: 'admin',
password: 'admin'
});
const rules: FormRules = {
@@ -116,7 +116,10 @@ const submitForm = (formEl: FormInstance | undefined) => {
if (!data) return;
console.log("login data", data, data.userInfo);
if (![1, 2].includes(data.userInfo?.roleId)) {
ElMessage.error('您所在用户组无权登录当前系统');
return;
}
ElMessage.success('登录成功');
localStorage.setItem('ms_username', data.userInfo?.username);
localStorage.setItem('ms_realname', data.userInfo?.realname);

View File

@@ -0,0 +1,191 @@
<template>
<div class="container">
<manageList :list-func="shopOrderApi.getOrderList" :custom-edit-handle="editHandle"
:export-func="shopOrderApi.exportOrderList" edit-permiss="shop-order-setting" />
<!-- 新增 / 编辑弹出框 -->
<el-dialog title="管理订单" v-model="visible" style="width: 60%; min-width: 280px;">
<el-row>
<el-col :span="12">
<p class="line-height">
<span class="row-index">订单ID</span>
{{ orderDetail.id || "" }}
</p>
<p class="line-height">
<span class="row-index">订单状态</span>
<el-tag effect="plain" round>{{ orderDetail.orderStatus || "" }}</el-tag>
{{ orderDetail.orderStatusCode || "" }}
</p>
<p class="line-height">
<span class="row-index">下单用户ID</span>
{{ orderDetail.userId || "" }}
</p>
<p class="line-height">
<span class="row-index">订单价格</span>
{{ orderDetail.orderPrice ? ('¥' + orderDetail.orderPrice) : "-" }}
</p>
<p class="line-height">
<span class="row-index">下单时间</span>
{{ orderDetail.orderDate || "-" }}
</p>
<p class="line-height">
<span class="row-index">订单取消时间</span>
{{ orderDetail.cancelDate || "-" }}
</p>
<p class="line-height">
<span class="row-index">订单支付时间</span>
{{ orderDetail.payDate || "-" }}
</p>
<p class="line-height">
<span class="row-index">订单发货时间</span>
{{ orderDetail.shipDate || "-" }}
</p>
<p class="line-height">
<span class="row-index">订单送达时间</span>
{{ orderDetail.deliverDate || "-" }}
</p>
<p class="line-height">
<span class="row-index">运单号</span>
<template
v-if="orderDetail.orderStatusCode != 'Processing' && orderDetail.orderStatusCode != 'Shipped'">
{{ orderDetail.expressId || "-" }}
</template>
<el-input v-else v-model="shippingInfo.expressId" placeholder="Please input" />
</p>
<p class="line-height">
<span class="row-index">发货备注</span>
<template
v-if="orderDetail.orderStatusCode != 'Processing' && orderDetail.orderStatusCode != 'Shipped'">
{{ orderDetail.comment || "-" }}
</template>
<el-input v-else v-model="shippingInfo.comment" placeholder="Please input" />
</p>
</el-col>
<el-col :span="12">
<p style="margin-bottom: 20px;">该订单中包含如下商品</p>
<div style="max-height:50vh; overflow-y: scroll;">
<el-card class="box-card" v-for="i in orderItem" :key="i.goodId"
style="--el-card-padding: 10px; margin-bottom: 8px;">
<div style="display: grid; grid-template-columns: 50px 1fr 60px; gap: 15px;">
<img :src="i.good.picUrl" style="width: 60px; height: 60px;" />
<div style="place-self: center left;">
<p>{{ i.good.goodsName }}</p>
<p style="color: grey; font-size: 12px;">商品ID: {{ i.goodId }}</p>
</div>
<div style="place-self: center; font-size: 16px;">{{ i.goodCount }} {{ i.good.unit }}</div>
</div>
</el-card>
</div>
</el-col>
</el-row>
<!-- 订单状态PENDING("Pending", "等待确认"), -->
<!-- 订单状态PROCESSING("Processing", "已支付,等待发货中"), -->
<!-- 订单状态SHIPPED("Shipped", "已发货,等待确认收货"), -->
<!-- 订单状态DELIVERED("Delivered", "已送达"), -->
<!-- 订单状态CANCELLED("Cancelled", "已取消"); -->
<!-- 该订单已被取消 -->
<template #footer>
<span class="dialog-footer">
<el-button v-if="orderDetail.orderStatusCode == 'Processing'" type="danger"
@click="withdrawOrder">取消发货并退款</el-button>
<el-button
v-if="orderDetail.orderStatusCode == 'Processing' || orderDetail.orderStatusCode == 'Shipped'"
type="primary" @click="saveEdit">保存发货信息</el-button>
<el-button @click="visible = false"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import manageList from '../components/manage-list.vue';
import * as shopOrderApi from '../api/shop-order';
import { ref, reactive, onMounted, computed } from 'vue';
import { FormInstance, FormRules, ElMessage, ElMessageBox } from 'element-plus';
import { Delete, Edit, Search, Plus, Filter, Download } from '@element-plus/icons-vue';
const visible: any = ref(false);
const orderDetail = ref({} as any);
const orderItem = ref([] as any);
const shippingInfo = ref({} as any); // 发货信息
let completeCallback: Function = () => { };
const editHandle = async function (tabIndex: number, row: any, refreshFunc: Function) {
console.log("tabIndex", tabIndex)
console.log("orderDetail", row)
orderDetail.value = row // 订单详情
shippingInfo.value.expressId = row.expressId
shippingInfo.value.comment = row.comment
completeCallback = refreshFunc
visible.value = true
shopOrderApi.getOrderDetail({
orderId: row.id
}).then(function (data) {
let orderGoods = {}
data.goods.forEach((good: any) => {
orderGoods[good.id] = good
});
orderItem.value = data.orderItem.map((item: any) => {
item.good = orderGoods[item.goodId]
return item
})
console.log("orderItem", orderItem.value)
})
}
const saveEdit = function () {
// 保存发货信息
ElMessageBox.confirm('确定要进行发货操作吗?发货后订单将不可撤销。(用户点击收货前仍可以修改发货信息)', '提示', { type: 'warning' })
.then(async () => {
shopOrderApi.deliverOrder({
orderId: orderDetail.value.id,
...shippingInfo.value
}).then(function (data) {
if (data) { // 如果出错则已经出提示了
ElMessage.success({ message: data })
visible.value = false
completeCallback()
}
})
})
.catch(() => { });
}
const withdrawOrder = function () {
// 取消订单
ElMessageBox.confirm('确定要取消此订单吗?该操作不可撤销。', '提示', { type: 'warning' })
.then(async () => {
shopOrderApi.withdrawOrder({
orderId: orderDetail.value.id,
...shippingInfo.value
}).then(function (data) {
if (data) { // 如果出错则已经出提示了
ElMessage.success({ message: data })
visible.value = false
completeCallback()
}
})
})
.catch(() => { });
}
</script>
<style>
.line-height {
line-height: 2.7em;
}
.row-index {
width: 110px;
display: inline-block;
}
</style>

View File

@@ -1,67 +1,63 @@
<template>
<div>
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="clearfix">
<span>基础信息</span>
</div>
</template>
<div class="info">
<div class="info-image" @click="showDialog">
<el-avatar :size="100" :src="avatarImg" />
<span class="info-edit">
<i class="el-icon-lx-camerafill"></i>
</span>
</div>
<div class="info-name">{{ name }}</div>
<!-- <div class="info-desc">不可能我的代码怎么可能会有bug</div> -->
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="clearfix">
<span>账户编辑</span>
</div>
</template>
<el-form label-width="90px">
<el-form-item label="用户名"> {{ name }} </el-form-item>
<el-form-item label="旧密码:">
<el-input type="password" v-model="form.old"></el-input>
</el-form-item>
<el-form-item label="新密码:">
<el-input type="password" v-model="form.new"></el-input>
</el-form-item>
<el-form-item label="确认密码:">
<el-input type="password" v-model="form.new1"></el-input>
</el-form-item>
<!-- <el-form-item label="个人简介:">
<el-input v-model="form.desc"></el-input>
</el-form-item> -->
<el-form-item>
<el-button type="primary" @click="onSubmit">保存</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
<el-dialog title="裁剪图片" v-model="dialogVisible" width="600px">
<vue-cropper ref="cropper" :src="imgSrc" :ready="cropImage" :zoom="cropImage" :cropmove="cropImage"
style="width: 100%; height: 400px"></vue-cropper>
<div>
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="clearfix">
<span>基础信息</span>
</div>
</template>
<div class="info">
<div class="info-image" @click="showDialog">
<el-avatar :size="100" :src="avatarImg" />
<span class="info-edit">
<i class="el-icon-lx-camerafill"></i>
</span>
</div>
<div class="info-name">{{ name }}</div>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="clearfix">
<span>账户编辑</span>
</div>
</template>
<el-form label-width="90px">
<el-form-item label="用户名:"> {{ name }} </el-form-item>
<el-form-item label="旧密码">
<el-input type="password" v-model="form.old"></el-input>
</el-form-item>
<el-form-item label="新密码:">
<el-input type="password" v-model="form.new"></el-input>
</el-form-item>
<el-form-item label="确认密码:">
<el-input type="password" v-model="form.new1"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">保存</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
<el-dialog title="裁剪图片" v-model="dialogVisible" width="600px">
<vue-cropper ref="cropper" :src="imgSrc" :ready="cropImage" :zoom="cropImage" :cropmove="cropImage"
style="width: 100%; height: 400px"></vue-cropper>
<template #footer>
<span class="dialog-footer">
<el-button class="crop-demo-btn" type="primary">选择图片
<input class="crop-input" type="file" name="image" accept="image/*" @change="setImage" />
</el-button>
<el-button type="primary" @click="saveAvatar">上传并保存</el-button>
</span>
</template>
</el-dialog>
</div>
<template #footer>
<span class="dialog-footer">
<el-button class="crop-demo-btn" type="primary">选择图片
<input class="crop-input" type="file" name="image" accept="image/*" @change="setImage" />
</el-button>
<el-button type="primary" @click="saveAvatar">上传并保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="user">
@@ -70,65 +66,52 @@ import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
import avatar from '../assets/img/img.jpg';
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import send_request from '../utils/send_request';
import * as userApi from '../api/user';
const name = localStorage.getItem('ms_username');
const user_id = localStorage.getItem('ms_user_id');
const form = reactive({
user_id: user_id,
old: '',
new: '',
new1: '',
desc: ''
user_id: user_id,
old: '',
new: '',
new1: '',
desc: ''
});
const onSubmit = async () => {
if (form.old == '' || form.new == '' || form.new1 == '') {
// 弹窗
ElMessageBox.confirm('输入为空,请检查', '提示', {
type: 'warning'
})
return
} else if (form.new != form.new1) {
// 弹窗
ElMessageBox.confirm('新密码2次输入的不相同', '提示', {
type: 'warning'
})
return
} else if (form.new == form.old) {
// 弹窗
ElMessageBox.confirm('新、旧密码相同', '提示', {
type: 'warning'
})
return
}
if (form.old == '' || form.new == '' || form.new1 == '') {
// 弹窗
ElMessageBox.confirm('输入为空,请检查', '提示', {
type: 'warning'
})
return
} else if (form.new != form.new1) {
// 弹窗
ElMessageBox.confirm('新密码2次输入的不相同', '提示', {
type: 'warning'
})
return
} else if (form.new == form.old) {
// 弹窗
ElMessageBox.confirm('新、旧密码相同', '提示', {
type: 'warning'
})
return
}
ElMessageBox.confirm('确认要修改密码吗?', '提示', {
type: 'warning'
})
.then(async () => {
console.log("send_request v1/user/alterPSW")
const loading = ElLoading.service({
lock: true,
text: '请稍候',
background: 'rgba(0, 0, 0, 0.7)',
});
await send_request('v1/user/alterPSW', "POST", {
"userId": form.user_id,
"oldPSW": form.old,
"newPSW": form.new
}, (data: any) => {
console.log(data);
ElMessage.success('删除成功');
// tableData.value.splice(index, 1);
})
loading.close();
})
.catch(() => {
ElMessage.success('删除失败');
});
return;
ElMessageBox.confirm('确认要修改密码吗?', '提示', {
type: 'warning'
}).then(async () => {
userApi.updatePwd({
oldpwd: form.old,
newpwd: form.new,
}).then((data) => {
data && ElMessage.success(data);
})
}).catch((err) => {
ElMessage.success('删除失败');
console.log("err", err)
});
return;
};
@@ -139,92 +122,92 @@ const dialogVisible = ref(false);
const cropper: any = ref();
const showDialog = () => {
dialogVisible.value = true;
imgSrc.value = avatarImg.value;
dialogVisible.value = true;
imgSrc.value = avatarImg.value;
};
const setImage = (e: any) => {
const file = e.target.files[0];
if (!file.type.includes('image/')) {
return;
}
const reader = new FileReader();
reader.onload = (event: any) => {
dialogVisible.value = true;
imgSrc.value = event.target.result;
cropper.value && cropper.value.replace(event.target.result);
};
reader.readAsDataURL(file);
const file = e.target.files[0];
if (!file.type.includes('image/')) {
return;
}
const reader = new FileReader();
reader.onload = (event: any) => {
dialogVisible.value = true;
imgSrc.value = event.target.result;
cropper.value && cropper.value.replace(event.target.result);
};
reader.readAsDataURL(file);
};
const cropImage = () => {
cropImg.value = cropper.value.getCroppedCanvas().toDataURL();
cropImg.value = cropper.value.getCroppedCanvas().toDataURL();
};
const saveAvatar = () => {
avatarImg.value = cropImg.value;
dialogVisible.value = false;
avatarImg.value = cropImg.value;
dialogVisible.value = false;
};
</script>
<style scoped>
.info {
text-align: center;
padding: 35px 0;
text-align: center;
padding: 35px 0;
}
.info-image {
position: relative;
margin: auto;
width: 100px;
height: 100px;
background: #f8f8f8;
border: 1px solid #eee;
border-radius: 50px;
overflow: hidden;
position: relative;
margin: auto;
width: 100px;
height: 100px;
background: #f8f8f8;
border: 1px solid #eee;
border-radius: 50px;
overflow: hidden;
}
.info-edit {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s ease;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s ease;
}
.info-edit i {
color: #eee;
font-size: 25px;
color: #eee;
font-size: 25px;
}
.info-image:hover .info-edit {
opacity: 1;
opacity: 1;
}
.info-name {
margin: 15px 0 10px;
font-size: 24px;
font-weight: 500;
color: #262626;
margin: 15px 0 10px;
font-size: 24px;
font-weight: 500;
color: #262626;
}
.crop-demo-btn {
position: relative;
position: relative;
}
.crop-input {
position: absolute;
width: 100px;
height: 40px;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
position: absolute;
width: 100px;
height: 40px;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
</style>

View File

@@ -1,20 +0,0 @@
import { createApp } from 'vue'
import { setGlobalDataPlugin } from '@tarojs/taro'
import './app.css'
const App = createApp({
onShow(options) { },
// 入口组件不需要实现 render 方法,即使实现了也会被 taro 所覆盖
})
// 全局变量 refer: https://docs.taro.zone/docs/come-from-miniapp#%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F
App.use(setGlobalDataPlugin, {
globalData: {
debugMode: true, // 是否展示调试内容
baseUrl: true
? "https://epp-prod.only4.work"
: "http://localhost", // 不带最后的 /
}
})
export default App

View File

@@ -1,9 +1,16 @@
server
{
server_name epp-prod.only4.work;
server_name epp.only4.work;
listen 80;
listen 443 ssl http2;
# 并发限制 限制当前站点最大并发数
limit_conn perserver 50;
# 单IP限制 限制单个IP访问最大并发数
limit_conn perip 10;
# 流量限制 限制每个请求的流量上限单位KB
limit_rate 8192k;
location / {
proxy_pass http://127.0.0.1:5203;

View File

@@ -22,9 +22,14 @@ App({
},
globalData: {
// debugMode: true, // 是否展示调试内容
baseUrl: true ? // Api 请求域名 不带最后的 /
"https://epp-prod.only4.work" :
"http://localhost",
/**
* FIXME 环境配置
*
* baseUrl
* - 线上环境:'https://epp.only4.work'
* - 开发环境:'http://localhost'
*/
baseUrl: "http://localhost", // Api 请求域名 不带最后的 /
userInfo: null
}
})

View File

@@ -1,97 +1,97 @@
var defaultTabbarItem = [
"pages/person/person",
"pages/residents/report",
"pages/index/index"
]
export function toggle(that) {
let pages = getCurrentPages()
let route = pages[pages.length - 1].route
console.log("[CustomTabBar] route", route)
if (!typeof that.getTabBar === 'function' || !that.getTabBar()) {
// wx.showModal({
// title: '版本太旧',
// content: '您的微信版本太旧,无法使用本小程序',
// showCancel: false,
// complete: (res) => {
// wx.exitMiniProgram()
// }
// })
return
}
// 找出要选择哪一项
let tabBar = that.getTabBar()
console.log("[CustomTabBar] tabBar", tabBar)
let data = tabBar.data
console.log("[CustomTabBar] data", data)
let showPagePathList = wx.getStorageSync('tabbarItem') || defaultTabbarItem
console.log("[CustomTabBar] showPagePathList", showPagePathList)
let list = tabBar.data.rawList.filter((page) => showPagePathList.includes(page.pagePath))
console.log("[CustomTabBar] tabBarList", list)
let selected = list ? list.indexOf(list.find(p => p.pagePath == route)) : -1
console.log("[CustomTabBar] selected", selected)
console.log("更新tabbar")
// 选中这一项
tabBar.setData({
selected: selected,
list: list
})
}
export function updateConfig(baseUrl) {
// 请求配置文件(用于审核时隐藏部分功能)
wx.request({
url: baseUrl + '/getConfig',
success(result) {
let data = result.data
if (data.tabbarItem) {
console.log("[CustomTabBar] tabbar数据拉取完毕")
wx.setStorageSync('tabbarItem', data.tabbarItem)
wx.setStorageSync('indexItem', data.indexItem)
const pages = getCurrentPages();
const indexPage = pages[0]
const currentPage = pages[pages.length - 1]
console.log("[CustomTabBar] indexPage", indexPage)
console.log("[CustomTabBar] currentPage", currentPage)
// // 更新tabbar
// toggle(currentPage)
// 刷新首页中包含了触发 toggle 的代码 此处不重复触发
// 刷新首页
indexPage && indexPage.onLoad()
// let tabBar = currentPage.getTabBar()
// tabBar.setData({
// list: tabBar.data.rawList.filter((page) => itemList.includes(page.pagePath))
// })
}
},
fail() {
wx.showModal({
title: '小程序启动失败',
content: '点击确认重试,若多次失败请检查网络连接',
complete: (res) => {
if (res.cancel) {
wx.exitMiniProgram()
}
if (res.confirm) {
wx.reLaunch({
url: 'pages/index/index',
})
}
}
})
}
})
}
var defaultTabbarItem = [
"pages/person/person",
"pages/residents/report",
"pages/index/index"
]
export function toggle(that) {
let pages = getCurrentPages()
let route = pages[pages.length - 1].route
console.log("[CustomTabBar] route", route)
if (!typeof that.getTabBar === 'function' || !that.getTabBar()) {
// wx.showModal({
// title: '版本太旧',
// content: '您的微信版本太旧,无法使用本小程序',
// showCancel: false,
// complete: (res) => {
// wx.exitMiniProgram()
// }
// })
return
}
// 找出要选择哪一项
let tabBar = that.getTabBar()
console.log("[CustomTabBar] tabBar", tabBar)
let data = tabBar.data
console.log("[CustomTabBar] data", data)
let showPagePathList = wx.getStorageSync('tabbarItem') || defaultTabbarItem
console.log("[CustomTabBar] showPagePathList", showPagePathList)
let list = tabBar.data.rawList.filter((page) => showPagePathList.includes(page.pagePath))
console.log("[CustomTabBar] tabBarList", list)
let selected = list ? list.indexOf(list.find(p => p.pagePath == route)) : -1
console.log("[CustomTabBar] selected", selected)
console.log("更新tabbar")
// 选中这一项
tabBar.setData({
selected: selected,
list: list
})
}
export function updateConfig(baseUrl) {
// 请求配置文件(用于审核时隐藏部分功能)
wx.request({
url: baseUrl + '/getConfig?v=2',
success(result) {
let data = result.data
if (data.tabbarItem) {
console.log("[CustomTabBar] tabbar数据拉取完毕")
wx.setStorageSync('tabbarItem', data.tabbarItem)
wx.setStorageSync('indexItem', data.indexItem)
const pages = getCurrentPages();
const indexPage = pages[0]
const currentPage = pages[pages.length - 1]
console.log("[CustomTabBar] indexPage", indexPage)
console.log("[CustomTabBar] currentPage", currentPage)
// // 更新tabbar
// toggle(currentPage)
// 刷新首页中包含了触发 toggle 的代码 此处不重复触发
// 刷新首页
indexPage && indexPage.onLoad()
// let tabBar = currentPage.getTabBar()
// tabBar.setData({
// list: tabBar.data.rawList.filter((page) => itemList.includes(page.pagePath))
// })
}
},
fail() {
wx.showModal({
title: '小程序启动失败',
content: '点击确认重试,若多次失败请检查网络连接',
complete: (res) => {
if (res.cancel) {
wx.exitMiniProgram()
}
if (res.confirm) {
wx.reLaunch({
url: 'pages/index/index',
})
}
}
})
}
})
}

View File

@@ -7,16 +7,16 @@
<view class="good-card-info">
<!-- <text class="good-title line-wrap">{{ goodinfo.goodsName }}</text> -->
<text class="good-title line-wrap">{{ goodinfo.brief }}</text>
<view class="good-price good-price-counter">
<!-- 原价 -->
<view class="good-price-symbol">¥</view>
<view class="good-price-number">{{ goodinfo.counterPrice }}</view>
</view>
<view class="good-price good-price-retail">
<!-- 售价 -->
<view style="height:0.12em;" class="good-price-line"></view>
<view class="good-price-symbol">¥</view>
<view class="good-price-number">{{ goodinfo.retailPrice }}</view>
</view>
<view class="good-price good-price-counter">
<!-- 原价 -->
<view style="height:0.12em;" class="good-price-line"></view>
<view class="good-price-symbol">¥</view>
<view class="good-price-number">{{ goodinfo.counterPrice }}</view>
</view>
</view>
</view>

View File

@@ -52,12 +52,12 @@
position: relative;
}
.good-price-counter {
.good-price-retail {
color: #fa4126;
font-size: 36rpx;
}
.good-price-retail {
.good-price-counter {
color: #bbbbbb;
font-size: 24rpx;
margin: 0 0 0 8rpx;

View File

@@ -137,7 +137,7 @@ Page({
filterList: filterList,
filterActiveName: "全部",
orderList: userOrder.orders.map(order => {
order.displayDate = order.orderDate.replace("T", " ")
order.displayDate = order.orderDate
return order
})
})
@@ -153,7 +153,7 @@ Page({
console.log("userOrder", userOrder)
this.setData({
orderList: userOrder.orders.map(order => {
order.displayDate = order.orderDate.replace("T", " ")
order.displayDate = order.orderDate
return order
})
})

View File

@@ -259,7 +259,7 @@ Page({
this.setData({
order: orderDetail.order,
orderGoodList: orderGoodList,
orderTime: orderDetail.order.orderDate.replace("T", " "),
orderTime: orderDetail.order.orderDate,
orderStatusCode: orderStatusCode,
orderPrice: orderPrice,
})

View File

@@ -7,20 +7,4 @@ TODO
文档中的TODO处理
论文可以参考一下https://blog.csdn.net/WeiXin_DZbishe/article/details/127091024
【中英论文下载】
强烈推荐使用谷歌浏览器
卡号356983353,密码270045
打开链接
www.mv616.xyz点击页面顶端【会员登录】
输入发给你的卡号卡密,点击【资源列表】然后点击【中文/英文数据库】
_________________________________________________________________
1每个数据库里面很多入口选择一个点击进去搜不到或者不能下载的换其他入口使用即可
2三小时内5星带10字以上好评告知客服免费送第三方查重工具哦
*不限制下载数量,遇到使用问题请咨询客服或者留言,不满意支持退款的哦!

3
分支说明.md Normal file
View File

@@ -0,0 +1,3 @@
main: 开发分支 域名为localhost; 门禁端小程序环境为develop
production: 生产分支 基于开发分支域名改为epp.only4.work; 门禁端小程序环境改为release
miniprogramAudit: 小程序提审分支 (基于生产分支,从后端动态隐藏小程序端社区码&小商店功能)

View File

@@ -1,26 +1,21 @@
> 使用 phpMyAdmin 导出,如果有修改,可以同步修改到论文正文
## apply1
## access_log
| 字段 | 类型 | 空 | 默认 | 注释 |
| :---------- | :---------- | :--- | :----- | :--------------------------- |
| id *(主键)* | int | 否 | | |
| stu_id | int | 是 | *NULL* | 用户id |
| issue | varchar(50) | 是 | *NULL* | 申请事由 |
| transport | varchar(20) | 是 | *NULL* | 出行方式 |
| place | varchar(50) | 是 | *NULL* | 目的地 |
| start_time | datetime | 是 | *NULL* | 开始时间 |
| end_time | datetime | 是 | *NULL* | 结束时间 |
| state | int | 是 | 0 | 状态(0:审批中,1:通过,2:驳回) |
| reason | varchar(20) | 是 | *NULL* | 驳回原因 |
表注释: *人员进出记录表*
### 索引
| 字段 | 类型 | 空 | 默认 | 注释 |
| :------------- | :---------------- | :--- | :---------------- | :------------------- |
| id | bigint | 否 | | 雪花id |
| time | datetime | 否 | CURRENT_TIMESTAMP | 进出时间 |
| user_id | int | 否 | | 用户id |
| user_real_name | varchar(255) | 是 | *NULL* | 用户真实姓名 |
| gate_id | bigint | 否 | | 大门id |
| type | enum('IN', 'OUT') | 是 | *NULL* | 类型(进门 OR 出门) |
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 10 | A | 否 | |
没有已定义的索引!
## community_gate
## gate
表注释: *社区大门*
@@ -32,38 +27,6 @@
### 索引
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
## count1
| 字段 | 类型 | 空 | 默认 | 注释 |
| :------------ | :------- | :--- | :----- | :------- |
| date *(主键)* | date | 否 | | 日期 |
| time | datetime | 是 | *NULL* | 时间 |
| in_num | int | 是 | *NULL* | 入校人数 |
| out_num | int | 是 | *NULL* | 出校人数 |
### 索引
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | date | 0 | A | 否 | |
## feedback1
| 字段 | 类型 | 空 | 默认 | 注释 |
| :---------- | :----------- | :--- | :----- | :--------- |
| id *(主键)* | int | 否 | | |
| title | varchar(255) | 是 | *NULL* | 反馈标题 |
| content | longtext | 是 | *NULL* | 反馈内容 |
| time | datetime | 是 | *NULL* | 反馈时间 |
| user_id | int | 是 | *NULL* | 反馈用户id |
| reply | varchar(255) | 是 | *NULL* | 回复 |
### 索引
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 5 | A | 否 | |
@@ -94,10 +57,10 @@
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :---------- | :---- | :--- | :--- | :---------- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 87 | A | 否 | |
| PRIMARY | BTREE | 是 | 否 | id | 89 | A | 否 | |
| category_id | BTREE | 否 | 否 | category_id | 8 | A | 是 | |
| brand_id | BTREE | 否 | 否 | brand | 67 | A | 是 | |
| sort_order | BTREE | 否 | 否 | sort_order | 25 | A | 是 | |
| brand_id | BTREE | 否 | 否 | brand | 69 | A | 是 | |
| sort_order | BTREE | 否 | 否 | sort_order | 27 | A | 是 | |
## goods_category
@@ -111,22 +74,7 @@
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 7 | A | 否 | |
## notice1
| 字段 | 类型 | 空 | 默认 | 注释 |
| :---------- | :----------- | :--- | :----- | :--- |
| id *(主键)* | int | 否 | | |
| title | varchar(255) | 是 | *NULL* | 标题 |
| content | longtext | 是 | *NULL* | 内容 |
| time | date | 是 | *NULL* | 时间 |
### 索引
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 5 | A | 否 | |
| PRIMARY | BTREE | 是 | 否 | id | 8 | A | 否 | |
## order
@@ -138,13 +86,18 @@
| order_status | enum('Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled') | 否 | | 订单状态 |
| order_price | decimal(10,2) | 否 | | 订单总金额 |
| pay_date | datetime | 是 | *NULL* | 订单支付时间 |
| cancel_date | datetime | 是 | *NULL* | 订单取消时间 |
| ship_date | datetime | 是 | *NULL* | 订单发货时间 |
| deliver_date | datetime | 是 | *NULL* | 订单送达时间 |
| express_id | varchar(500) | 是 | *NULL* | 快递单号 |
| comment | text | 是 | *NULL* | 发货备注 |
### 索引
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :------ | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 4 | A | 否 | |
| id | BTREE | 是 | 否 | id | 4 | A | 否 | |
| PRIMARY | BTREE | 是 | 否 | id | 15 | A | 否 | |
| id | BTREE | 是 | 否 | id | 15 | A | 否 | |
| user_id | BTREE | 否 | 否 | user_id | 1 | A | 否 | |
## order_detail
@@ -161,9 +114,9 @@
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------- | :---- | :--- | :--- | :------- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 10 | A | 否 | |
| order_id | BTREE | 否 | 否 | order_id | 4 | A | 否 | |
| good_id | BTREE | 否 | 否 | good_id | 2 | A | 否 | |
| PRIMARY | BTREE | 是 | 否 | id | 21 | A | 否 | |
| order_id | BTREE | 否 | 否 | order_id | 15 | A | 否 | |
| good_id | BTREE | 否 | 否 | good_id | 9 | A | 否 | |
## report
@@ -180,7 +133,20 @@
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 1 | A | 否 | |
| PRIMARY | BTREE | 是 | 否 | id | 12 | A | 否 | |
## role
| 字段 | 类型 | 空 | 默认 | 注释 |
| :---------- | :----------- | :--- | :--- | :------- |
| id *(主键)* | int | 否 | | role_id |
| role_name | varchar(255) | 否 | | roleName |
### 索引
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 6 | A | 否 | |
## setting
@@ -198,42 +164,27 @@
## user
| 字段 | 类型 | 空 | 默认 | 注释 |
| :-------------- | :----------- | :--- | :----- | :----------------------------------------------------------- |
| id *(主键)* | int | 否 | | |
| username | varchar(20) | 否 | | 用户名 |
| password | varchar(255) | 否 | | 密码 |
| realname | varchar(20) | 是 | *NULL* | 真实姓名 |
| id_number | varchar(18) | 是 | *NULL* | 身份证号 |
| phone_number | varchar(11) | 是 | *NULL* | 手机号 |
| role | int | 否 | | 角色 (0-超级管理员 1-工作人员 2-社区居民_房主 3-社区居民_家庭成员 4-社区居民_租客 5-访客) |
| building_id | varchar(255) | 是 | *NULL* | 门栋号+单元号 |
| doorplate | varchar(255) | 是 | *NULL* | 门牌号 |
| permission | int | 否 | 0 | 进出权限 (0-无 1-继承(普通居民) 2-永久 3-限时) |
| permission_time | datetime | 是 | *NULL* | 进出权限失效时间 |
### 索引
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 1 | A | 否 | |
## visitor1
| 字段 | 类型 | 空 | 默认 | 注释 |
| :---------- | :----------- | :--- | :----- | :---------------------------------- |
| id *(主键)* | int | 否 | | |
| time | datetime | 是 | *NULL* | 预约时间 |
| phone | varchar(20) | 是 | *NULL* | 预约用户id |
| issue | varchar(255) | 是 | *NULL* | 预约事由 |
| meet_name | varchar(20) | 是 | *NULL* | 会见人姓名 |
| create_time | datetime | 是 | *NULL* | 创建时间 |
| state | int | 是 | 0 | 状态0审批中1成功2驳回 |
| 字段 | 类型 | 空 | 默认 | 注释 |
| :-------------- | :----------- | :--- | :----- | :--------------------------------------------- |
| id *(主键)* | int | 否 | | |
| username | varchar(20) | 否 | | 用户名 |
| password | varchar(255) | 否 | | 密码 |
| realname | varchar(20) | 是 | *NULL* | 真实姓名 |
| id_number | varchar(18) | 是 | *NULL* | 身份证号 |
| phone_number | varchar(20) | 是 | *NULL* | 手机号 |
| role_id | int | 否 | | 角色id |
| building_id | varchar(255) | 是 | *NULL* | 门栋号+单元号 |
| doorplate | varchar(255) | 是 | *NULL* | 门牌号 |
| permission | varchar(20) | 否 | 0 | 进出权限 (0-无 1-继承(普通居民) 2-永久 3-限时) |
| permission_time | datetime | 是 | *NULL* | 进出权限失效时间 |
| wx_code | varchar(255) | 是 | *NULL* | 微信登录授权码 |
### 索引
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------ | :---- | :--- | :--- | :--- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 1 | A | 否 | |
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| :------- | :---- | :--- | :--- | :------- | :--- | :------- | :--- | :--- |
| PRIMARY | BTREE | 是 | 否 | id | 1 | A | 否 | |
| username | BTREE | 是 | 否 | username | 1 | A | 否 | |
| wx_code | BTREE | 是 | 否 | wx_code | 1 | A | 是 | |