Commit e62495f65076dde537b559b6b39a1fc9eaa0eedd

Authored by YunaiV
2 parents deded703 860d0d01

Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro

Showing 141 changed files with 8690 additions and 185 deletions
@@ -46,6 +46,7 @@ @@ -46,6 +46,7 @@
46 <easyexcel.verion>2.2.7</easyexcel.verion> 46 <easyexcel.verion>2.2.7</easyexcel.verion>
47 <velocity.version>2.2</velocity.version> 47 <velocity.version>2.2</velocity.version>
48 <screw.version>1.0.5</screw.version> 48 <screw.version>1.0.5</screw.version>
  49 + <!-- 三方云服务相关 -->
49 </properties> 50 </properties>
50 51
51 <!-- 依赖声明 --> 52 <!-- 依赖声明 -->
@@ -271,6 +272,27 @@ @@ -271,6 +272,27 @@
271 <version>${screw.version}</version> 272 <version>${screw.version}</version>
272 </dependency> 273 </dependency>
273 274
  275 + <!-- 三方云服务相关 -->
  276 +
  277 + <!-- SMS SDK begin -->
  278 + <dependency>
  279 + <groupId>com.yunpian.sdk</groupId>
  280 + <artifactId>yunpian-java-sdk</artifactId>
  281 + <version>1.2.7</version>
  282 + </dependency>
  283 +
  284 + <dependency>
  285 + <groupId>com.aliyun</groupId>
  286 + <artifactId>aliyun-java-sdk-core</artifactId>
  287 + <version>4.5.18</version>
  288 + </dependency>
  289 + <dependency>
  290 + <groupId>com.aliyun</groupId>
  291 + <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
  292 + <version>2.1.0</version>
  293 + </dependency>
  294 + <!-- SMS SDK end -->
  295 +
274 </dependencies> 296 </dependencies>
275 297
276 298
ruoyi-ui/.env.demo1024 0 → 100644
  1 +NODE_ENV = production
  2 +
  3 +# 测试环境配置
  4 +ENV = 'staging'
  5 +
  6 +# 芋道管理系统/测试环境
  7 +VUE_APP_BASE_API = 'http://127.0.0.1:48080'
ruoyi-ui/package.json
@@ -8,6 +8,7 @@ @@ -8,6 +8,7 @@
8 "dev": "vue-cli-service serve", 8 "dev": "vue-cli-service serve",
9 "build:prod": "vue-cli-service build", 9 "build:prod": "vue-cli-service build",
10 "build:stage": "vue-cli-service build --mode staging", 10 "build:stage": "vue-cli-service build --mode staging",
  11 + "build:demo1024": "vue-cli-service build --mode demo1024",
11 "preview": "node build/index.js --preview", 12 "preview": "node build/index.js --preview",
12 "lint": "eslint --ext .js,.vue src" 13 "lint": "eslint --ext .js,.vue src"
13 }, 14 },
ruoyi-ui/src/api/system/sms/smsChannel.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +// 创建短信渠道
  4 +export function createSmsChannel(data) {
  5 + return request({
  6 + url: '/system/sms-channel/create',
  7 + method: 'post',
  8 + data: data
  9 + })
  10 +}
  11 +
  12 +// 更新短信渠道
  13 +export function updateSmsChannel(data) {
  14 + return request({
  15 + url: '/system/sms-channel/update',
  16 + method: 'put',
  17 + data: data
  18 + })
  19 +}
  20 +
  21 +// 删除短信渠道
  22 +export function deleteSmsChannel(id) {
  23 + return request({
  24 + url: '/system/sms-channel/delete?id=' + id,
  25 + method: 'delete'
  26 + })
  27 +}
  28 +
  29 +// 获得短信渠道
  30 +export function getSmsChannel(id) {
  31 + return request({
  32 + url: '/system/sms-channel/get?id=' + id,
  33 + method: 'get'
  34 + })
  35 +}
  36 +
  37 +// 获得短信渠道分页
  38 +export function getSmsChannelPage(query) {
  39 + return request({
  40 + url: '/system/sms-channel/page',
  41 + method: 'get',
  42 + params: query
  43 + })
  44 +}
  45 +
  46 +// 获得短信渠道精简列表
  47 +export function getSimpleSmsChannels() {
  48 + return request({
  49 + url: '/system/sms-channel/list-all-simple',
  50 + method: 'get',
  51 + })
  52 +}
ruoyi-ui/src/api/system/sms/smsLog.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +// 获得短信日志分页
  4 +export function getSmsLogPage(query) {
  5 + return request({
  6 + url: '/system/sms-log/page',
  7 + method: 'get',
  8 + params: query
  9 + })
  10 +}
  11 +
  12 +// 导出短信日志 Excel
  13 +export function exportSmsLogExcel(query) {
  14 + return request({
  15 + url: '/system/sms-log/export-excel',
  16 + method: 'get',
  17 + params: query,
  18 + responseType: 'blob'
  19 + })
  20 +}
ruoyi-ui/src/api/system/sms/smsTemplate.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +// 创建短信模板
  4 +export function createSmsTemplate(data) {
  5 + return request({
  6 + url: '/system/sms-template/create',
  7 + method: 'post',
  8 + data: data
  9 + })
  10 +}
  11 +
  12 +// 更新短信模板
  13 +export function updateSmsTemplate(data) {
  14 + return request({
  15 + url: '/system/sms-template/update',
  16 + method: 'put',
  17 + data: data
  18 + })
  19 +}
  20 +
  21 +// 删除短信模板
  22 +export function deleteSmsTemplate(id) {
  23 + return request({
  24 + url: '/system/sms-template/delete?id=' + id,
  25 + method: 'delete'
  26 + })
  27 +}
  28 +
  29 +// 获得短信模板
  30 +export function getSmsTemplate(id) {
  31 + return request({
  32 + url: '/system/sms-template/get?id=' + id,
  33 + method: 'get'
  34 + })
  35 +}
  36 +
  37 +// 获得短信模板分页
  38 +export function getSmsTemplatePage(query) {
  39 + return request({
  40 + url: '/system/sms-template/page',
  41 + method: 'get',
  42 + params: query
  43 + })
  44 +}
  45 +
  46 +// 创建短信模板
  47 +export function sendSms(data) {
  48 + return request({
  49 + url: '/system/sms-template/send-sms',
  50 + method: 'post',
  51 + data: data
  52 + })
  53 +}
  54 +
  55 +// 导出短信模板 Excel
  56 +export function exportSmsTemplateExcel(query) {
  57 + return request({
  58 + url: '/system/sms-template/export-excel',
  59 + method: 'get',
  60 + params: query,
  61 + responseType: 'blob'
  62 + })
  63 +}
  64 +
ruoyi-ui/src/utils/dict.js
@@ -17,6 +17,10 @@ export const DICT_TYPE = { @@ -17,6 +17,10 @@ export const DICT_TYPE = {
17 SYS_OPERATE_TYPE: 'sys_operate_type', 17 SYS_OPERATE_TYPE: 'sys_operate_type',
18 SYS_LOGIN_RESULT: 'sys_login_result', 18 SYS_LOGIN_RESULT: 'sys_login_result',
19 SYS_CONFIG_TYPE: 'sys_config_type', 19 SYS_CONFIG_TYPE: 'sys_config_type',
  20 + SYS_SMS_CHANNEL_CODE: 'sys_sms_channel_code',
  21 + SYS_SMS_TEMPLATE_TYPE: 'sys_sms_template_type',
  22 + SYS_SMS_SEND_STATUS: 'sys_sms_send_status',
  23 + SYS_SMS_RECEIVE_STATUS: 'sys_sms_receive_status',
20 24
21 INF_REDIS_TIMEOUT_TYPE: 'inf_redis_timeout_type', 25 INF_REDIS_TIMEOUT_TYPE: 'inf_redis_timeout_type',
22 INF_JOB_STATUS: 'inf_job_status', 26 INF_JOB_STATUS: 'inf_job_status',
ruoyi-ui/src/views/system/operatelog/index.vue
@@ -163,7 +163,6 @@ export default { @@ -163,7 +163,6 @@ export default {
163 businessType: undefined, 163 businessType: undefined,
164 status: undefined 164 status: undefined
165 }, 165 },
166 -  
167 }; 166 };
168 }, 167 },
169 created() { 168 created() {
ruoyi-ui/src/views/system/sms/smsChannel.vue 0 → 100644
  1 +<template>
  2 + <div class="app-container">
  3 +
  4 + <!-- 搜索工作栏 -->
  5 + <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
  6 + <el-form-item label="短信签名" prop="signature">
  7 + <el-input v-model="queryParams.signature" placeholder="请输入短信签名" clearable size="small" @keyup.enter.native="handleQuery"/>
  8 + </el-form-item>
  9 + <el-form-item label="启用状态" prop="status">
  10 + <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable size="small">
  11 + <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
  12 + :key="dict.value" :label="dict.label" :value="dict.value"/>
  13 + </el-select>
  14 + </el-form-item>
  15 + <el-form-item label="创建时间">
  16 + <el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
  17 + type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
  18 + </el-form-item>
  19 + <el-form-item>
  20 + <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  21 + <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  22 + </el-form-item>
  23 + </el-form>
  24 +
  25 + <!-- 操作工具栏 -->
  26 + <el-row :gutter="10" class="mb8">
  27 + <el-col :span="1.5">
  28 + <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
  29 + v-hasPermi="['system:sms-channel:create']">新增</el-button>
  30 + </el-col>
  31 + <el-col :span="1.5">
  32 + <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
  33 + v-hasPermi="['system:sms-channel:export']">导出</el-button>
  34 + </el-col>
  35 + <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  36 + </el-row>
  37 +
  38 + <!-- 列表 -->
  39 + <el-table v-loading="loading" :data="list">
  40 + <el-table-column label="编号" align="center" prop="id" />
  41 + <el-table-column label="短信签名" align="center" prop="signature" />
  42 + <el-table-column label="渠道编码" align="center" prop="code">
  43 + <template slot-scope="scope">
  44 + <span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.code) }}</span>
  45 + </template>
  46 + </el-table-column>>
  47 + <el-table-column label="启用状态" align="center" prop="status">
  48 + <template slot-scope="scope">
  49 + <span>{{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }}</span>
  50 + </template>
  51 + </el-table-column>>
  52 + <el-table-column label="备注" align="center" prop="remark" />
  53 + <el-table-column label="短信 API 的账号" align="center" prop="apiKey" />
  54 + <el-table-column label="短信 API 的秘钥" align="center" prop="apiSecret" />
  55 + <el-table-column label="短信发送回调 URL" align="center" prop="callbackUrl" />
  56 + <el-table-column label="创建时间" align="center" prop="createTime" width="180">
  57 + <template slot-scope="scope">
  58 + <span>{{ parseTime(scope.row.createTime) }}</span>
  59 + </template>
  60 + </el-table-column>
  61 + <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  62 + <template slot-scope="scope">
  63 + <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
  64 + v-hasPermi="['system:sms-channel:update']">修改</el-button>
  65 + <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
  66 + v-hasPermi="['system:sms-channel:delete']">删除</el-button>
  67 + </template>
  68 + </el-table-column>
  69 + </el-table>
  70 + <!-- 分页组件 -->
  71 + <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
  72 + @pagination="getList"/>
  73 +
  74 + <!-- 对话框(添加 / 修改) -->
  75 + <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
  76 + <el-form ref="form" :model="form" :rules="rules" label-width="80px">
  77 + <el-form-item label="短信签名" prop="signature">
  78 + <el-input v-model="form.signature" placeholder="请输入短信签名" />
  79 + </el-form-item>
  80 + <el-form-item label="渠道编码" prop="code">
  81 + <el-input v-model="form.code" placeholder="请输入渠道编码" />
  82 + </el-form-item>
  83 + <el-form-item label="启用状态">
  84 + <el-radio-group v-model="form.status">
  85 + <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
  86 + :key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
  87 + </el-radio-group>
  88 + </el-form-item>
  89 + <el-form-item label="备注" prop="remark">
  90 + <el-input v-model="form.remark" placeholder="请输入备注" />
  91 + </el-form-item>
  92 + <el-form-item label="短信 API 的账号" prop="apiKey">
  93 + <el-input v-model="form.apiKey" placeholder="请输入短信 API 的账号" />
  94 + </el-form-item>
  95 + <el-form-item label="短信 API 的秘钥" prop="apiSecret">
  96 + <el-input v-model="form.apiSecret" placeholder="请输入短信 API 的秘钥" />
  97 + </el-form-item>
  98 + <el-form-item label="短信发送回调 URL" prop="callbackUrl">
  99 + <el-input v-model="form.callbackUrl" placeholder="请输入短信发送回调 URL" />
  100 + </el-form-item>
  101 + </el-form>
  102 + <div slot="footer" class="dialog-footer">
  103 + <el-button type="primary" @click="submitForm">确 定</el-button>
  104 + <el-button @click="cancel">取 消</el-button>
  105 + </div>
  106 + </el-dialog>
  107 + </div>
  108 +</template>
  109 +
  110 +<script>
  111 +import { createSmsChannel, updateSmsChannel, deleteSmsChannel, getSmsChannel, getSmsChannelPage,
  112 + getSimpleSmsChannels } from "@/api/system/sms/smsChannel";
  113 +
  114 +export default {
  115 + name: "SmsChannel",
  116 + components: {
  117 + },
  118 + data() {
  119 + return {
  120 + // 遮罩层
  121 + loading: true,
  122 + // 显示搜索条件
  123 + showSearch: true,
  124 + // 总条数
  125 + total: 0,
  126 + // 短信渠道列表
  127 + list: [],
  128 + // 弹出层标题
  129 + title: "",
  130 + // 是否显示弹出层
  131 + open: false,
  132 + dateRangeCreateTime: [],
  133 + // 查询参数
  134 + queryParams: {
  135 + pageNo: 1,
  136 + pageSize: 10,
  137 + signature: null,
  138 + status: null,
  139 + },
  140 + // 表单参数
  141 + form: {},
  142 + // 表单校验
  143 + rules: {
  144 + signature: [{ required: true, message: "短信签名不能为空", trigger: "blur" }],
  145 + code: [{ required: true, message: "渠道编码不能为空", trigger: "blur" }],
  146 + status: [{ required: true, message: "启用状态不能为空", trigger: "blur" }],
  147 + apiKey: [{ required: true, message: "短信 API 的账号不能为空", trigger: "blur" }],
  148 + },
  149 + };
  150 + },
  151 + created() {
  152 + this.getList();
  153 + },
  154 + methods: {
  155 + /** 查询列表 */
  156 + getList() {
  157 + this.loading = true;
  158 + // 处理查询参数
  159 + let params = {...this.queryParams};
  160 + this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
  161 + // 执行查询
  162 + getSmsChannelPage(params).then(response => {
  163 + this.list = response.data.list;
  164 + this.total = response.data.total;
  165 + this.loading = false;
  166 + });
  167 + },
  168 + /** 取消按钮 */
  169 + cancel() {
  170 + this.open = false;
  171 + this.reset();
  172 + },
  173 + /** 表单重置 */
  174 + reset() {
  175 + this.form = {
  176 + id: undefined,
  177 + signature: undefined,
  178 + code: undefined,
  179 + status: undefined,
  180 + remark: undefined,
  181 + apiKey: undefined,
  182 + apiSecret: undefined,
  183 + callbackUrl: undefined,
  184 + };
  185 + this.resetForm("form");
  186 + },
  187 + /** 搜索按钮操作 */
  188 + handleQuery() {
  189 + this.queryParams.pageNo = 1;
  190 + this.getList();
  191 + },
  192 + /** 重置按钮操作 */
  193 + resetQuery() {
  194 + this.dateRangeCreateTime = [];
  195 + this.resetForm("queryForm");
  196 + this.handleQuery();
  197 + },
  198 + /** 新增按钮操作 */
  199 + handleAdd() {
  200 + this.reset();
  201 + this.open = true;
  202 + this.title = "添加短信渠道";
  203 + },
  204 + /** 修改按钮操作 */
  205 + handleUpdate(row) {
  206 + this.reset();
  207 + const id = row.id;
  208 + getSmsChannel(id).then(response => {
  209 + this.form = response.data;
  210 + this.open = true;
  211 + this.title = "修改短信渠道";
  212 + });
  213 + },
  214 + /** 提交按钮 */
  215 + submitForm() {
  216 + this.$refs["form"].validate(valid => {
  217 + if (!valid) {
  218 + return;
  219 + }
  220 + // 修改的提交
  221 + if (this.form.id != null) {
  222 + updateSmsChannel(this.form).then(response => {
  223 + this.msgSuccess("修改成功");
  224 + this.open = false;
  225 + this.getList();
  226 + });
  227 + return;
  228 + }
  229 + // 添加的提交
  230 + createSmsChannel(this.form).then(response => {
  231 + this.msgSuccess("新增成功");
  232 + this.open = false;
  233 + this.getList();
  234 + });
  235 + });
  236 + },
  237 + /** 删除按钮操作 */
  238 + handleDelete(row) {
  239 + const id = row.id;
  240 + this.$confirm('是否确认删除短信渠道编号为"' + id + '"的数据项?', "警告", {
  241 + confirmButtonText: "确定",
  242 + cancelButtonText: "取消",
  243 + type: "warning"
  244 + }).then(function() {
  245 + return deleteSmsChannel(id);
  246 + }).then(() => {
  247 + this.getList();
  248 + this.msgSuccess("删除成功");
  249 + })
  250 + },
  251 + /** 导出按钮操作 */
  252 + handleExport() {
  253 + // 处理查询参数
  254 + let params = {...this.queryParams};
  255 + params.pageNo = undefined;
  256 + params.pageSize = undefined;
  257 + this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
  258 + // 执行导出
  259 + this.$confirm('是否确认导出所有短信渠道数据项?', "警告", {
  260 + confirmButtonText: "确定",
  261 + cancelButtonText: "取消",
  262 + type: "warning"
  263 + }).then(function() {
  264 + return exportSmsChannelExcel(params);
  265 + }).then(response => {
  266 + this.downloadExcel(response, '短信渠道.xls');
  267 + })
  268 + }
  269 + }
  270 +};
  271 +</script><template>
  272 + <div class="app-container">
  273 +
  274 + <!-- 搜索工作栏 -->
  275 + <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
  276 + <el-form-item label="短信签名" prop="signature">
  277 + <el-input v-model="queryParams.signature" placeholder="请输入短信签名" clearable size="small" @keyup.enter.native="handleQuery"/>
  278 + </el-form-item>
  279 + <el-form-item label="启用状态" prop="status">
  280 + <el-select v-model="queryParams.status" placeholder="请选择启用状态" clearable size="small">
  281 + <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
  282 + :key="dict.value" :label="dict.label" :value="dict.value"/>
  283 + </el-select>
  284 + </el-form-item>
  285 + <el-form-item label="创建时间">
  286 + <el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
  287 + type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
  288 + </el-form-item>
  289 + <el-form-item>
  290 + <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  291 + <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  292 + </el-form-item>
  293 + </el-form>
  294 +
  295 + <!-- 操作工具栏 -->
  296 + <el-row :gutter="10" class="mb8">
  297 + <el-col :span="1.5">
  298 + <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
  299 + v-hasPermi="['system:sms-channel:create']">新增</el-button>
  300 + </el-col>
  301 + <el-col :span="1.5">
  302 + <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
  303 + v-hasPermi="['system:sms-channel:export']">导出</el-button>
  304 + </el-col>
  305 + <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  306 + </el-row>
  307 +
  308 + <!-- 列表 -->
  309 + <el-table v-loading="loading" :data="list">
  310 + <el-table-column label="编号" align="center" prop="id" />
  311 + <el-table-column label="短信签名" align="center" prop="signature" />
  312 + <el-table-column label="渠道编码" align="center" prop="code">
  313 + <template slot-scope="scope">
  314 + <span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.code) }}</span>
  315 + </template>
  316 + </el-table-column>>
  317 + <el-table-column label="启用状态" align="center" prop="status">
  318 + <template slot-scope="scope">
  319 + <span>{{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }}</span>
  320 + </template>
  321 + </el-table-column>>
  322 + <el-table-column label="备注" align="center" prop="remark" />
  323 + <el-table-column label="创建时间" align="center" prop="createTime" width="180">
  324 + <template slot-scope="scope">
  325 + <span>{{ parseTime(scope.row.createTime) }}</span>
  326 + </template>
  327 + </el-table-column>
  328 + <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  329 + <template slot-scope="scope">
  330 + <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
  331 + v-hasPermi="['system:sms-channel:update']">修改</el-button>
  332 + <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
  333 + v-hasPermi="['system:sms-channel:delete']">删除</el-button>
  334 + </template>
  335 + </el-table-column>
  336 + </el-table>
  337 + <!-- 分页组件 -->
  338 + <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
  339 + @pagination="getList"/>
  340 +
  341 + <!-- 对话框(添加 / 修改) -->
  342 + <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
  343 + <el-form ref="form" :model="form" :rules="rules" label-width="130px">
  344 + <el-form-item label="短信签名" prop="signature">
  345 + <el-input v-model="form.signature" placeholder="请输入短信签名" />
  346 + </el-form-item>
  347 + <el-form-item label="渠道编码" prop="code">
  348 + <el-select v-model="form.code" placeholder="请选择渠道编码" :disabled="form.id > 0">
  349 + <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_CHANNEL_CODE)"
  350 + :key="dict.value" :label="dict.label" :value="dict.value" />
  351 + </el-select>
  352 + </el-form-item>
  353 + <el-form-item label="启用状态">
  354 + <el-radio-group v-model="form.status">
  355 + <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
  356 + :key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
  357 + </el-radio-group>
  358 + </el-form-item>
  359 + <el-form-item label="备注" prop="remark">
  360 + <el-input v-model="form.remark" placeholder="请输入备注" />
  361 + </el-form-item>
  362 + <el-form-item label="短信 API 的账号" prop="apiKey">
  363 + <el-input v-model="form.apiKey" placeholder="请输入短信 API 的账号" />
  364 + </el-form-item>
  365 + <el-form-item v-if="form.code !== 'YUN_PIAN'" label="短信 API 的秘钥" prop="apiSecret">
  366 + <el-input v-model="form.apiSecret" placeholder="请输入短信 API 的秘钥" />
  367 + </el-form-item>
  368 + <el-form-item label="短信发送回调 URL" prop="callbackUrl">
  369 + <el-input v-model="form.callbackUrl" placeholder="请输入短信发送回调 URL" />
  370 + </el-form-item>
  371 + </el-form>
  372 + <div slot="footer" class="dialog-footer">
  373 + <el-button type="primary" @click="submitForm">确 定</el-button>
  374 + <el-button @click="cancel">取 消</el-button>
  375 + </div>
  376 + </el-dialog>
  377 + </div>
  378 +</template>
  379 +
  380 +<script>
  381 +import { createSmsChannel, updateSmsChannel, deleteSmsChannel, getSmsChannel, getSmsChannelPage } from "@/api/system/sms/smsChannel";
  382 +
  383 +export default {
  384 + name: "SmsChannel",
  385 + components: {
  386 + },
  387 + data() {
  388 + return {
  389 + // 遮罩层
  390 + loading: true,
  391 + // 显示搜索条件
  392 + showSearch: true,
  393 + // 总条数
  394 + total: 0,
  395 + // 短信渠道列表
  396 + list: [],
  397 + // 弹出层标题
  398 + title: "",
  399 + // 是否显示弹出层
  400 + open: false,
  401 + dateRangeCreateTime: [],
  402 + // 查询参数
  403 + queryParams: {
  404 + pageNo: 1,
  405 + pageSize: 10,
  406 + signature: null,
  407 + status: null,
  408 + },
  409 + // 表单参数
  410 + form: {},
  411 + // 表单校验
  412 + apiKeyEnableChannelCodes: ['YUN_PIAN'],
  413 + rules: {
  414 + signature: [{ required: true, message: "短信签名不能为空", trigger: "blur" }],
  415 + code: [{ required: true, message: "渠道编码不能为空", trigger: "blur" }],
  416 + status: [{ required: true, message: "启用状态不能为空", trigger: "blur" }],
  417 + apiKey: [{ required: true, message: "短信 API 的账号不能为空", trigger: "blur" }],
  418 + apiSecret: [{ required: true, message: "短信 API 的秘钥不能为空", trigger: "blur" }],
  419 + }
  420 + };
  421 + },
  422 + created() {
  423 + this.getList();
  424 + },
  425 + methods: {
  426 + /** 查询列表 */
  427 + getList() {
  428 + this.loading = true;
  429 + // 处理查询参数
  430 + let params = {...this.queryParams};
  431 + this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
  432 + // 执行查询
  433 + getSmsChannelPage(params).then(response => {
  434 + this.list = response.data.list;
  435 + this.total = response.data.total;
  436 + this.loading = false;
  437 + });
  438 + },
  439 + /** 取消按钮 */
  440 + cancel() {
  441 + this.open = false;
  442 + this.reset();
  443 + },
  444 + /** 表单重置 */
  445 + reset() {
  446 + this.form = {
  447 + id: undefined,
  448 + signature: undefined,
  449 + code: undefined,
  450 + status: undefined,
  451 + remark: undefined,
  452 + apiKey: undefined,
  453 + apiSecret: undefined,
  454 + callbackUrl: undefined,
  455 + };
  456 + this.resetForm("form");
  457 + },
  458 + /** 搜索按钮操作 */
  459 + handleQuery() {
  460 + this.queryParams.pageNo = 1;
  461 + this.getList();
  462 + },
  463 + /** 重置按钮操作 */
  464 + resetQuery() {
  465 + this.dateRangeCreateTime = [];
  466 + this.resetForm("queryForm");
  467 + this.handleQuery();
  468 + },
  469 + /** 新增按钮操作 */
  470 + handleAdd() {
  471 + this.reset();
  472 + this.open = true;
  473 + this.title = "添加短信渠道";
  474 + },
  475 + /** 修改按钮操作 */
  476 + handleUpdate(row) {
  477 + this.reset();
  478 + const id = row.id;
  479 + getSmsChannel(id).then(response => {
  480 + this.form = response.data;
  481 + this.open = true;
  482 + this.title = "修改短信渠道";
  483 + });
  484 + },
  485 + /** 提交按钮 */
  486 + submitForm() {
  487 + this.$refs["form"].validate(valid => {
  488 + if (!valid) {
  489 + return;
  490 + }
  491 + // 修改的提交
  492 + if (this.form.id != null) {
  493 + updateSmsChannel(this.form).then(response => {
  494 + this.msgSuccess("修改成功");
  495 + this.open = false;
  496 + this.getList();
  497 + });
  498 + return;
  499 + }
  500 + // 添加的提交
  501 + createSmsChannel(this.form).then(response => {
  502 + this.msgSuccess("新增成功");
  503 + this.open = false;
  504 + this.getList();
  505 + });
  506 + });
  507 + },
  508 + /** 删除按钮操作 */
  509 + handleDelete(row) {
  510 + const id = row.id;
  511 + this.$confirm('是否确认删除短信渠道编号为"' + id + '"的数据项?', "警告", {
  512 + confirmButtonText: "确定",
  513 + cancelButtonText: "取消",
  514 + type: "warning"
  515 + }).then(function() {
  516 + return deleteSmsChannel(id);
  517 + }).then(() => {
  518 + this.getList();
  519 + this.msgSuccess("删除成功");
  520 + })
  521 + },
  522 + /** 导出按钮操作 */
  523 + handleExport() {
  524 + // 处理查询参数
  525 + let params = {...this.queryParams};
  526 + params.pageNo = undefined;
  527 + params.pageSize = undefined;
  528 + this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
  529 + // 执行导出
  530 + this.$confirm('是否确认导出所有短信渠道数据项?', "警告", {
  531 + confirmButtonText: "确定",
  532 + cancelButtonText: "取消",
  533 + type: "warning"
  534 + }).then(function() {
  535 + return exportSmsChannelExcel(params);
  536 + }).then(response => {
  537 + this.downloadExcel(response, '短信渠道.xls');
  538 + })
  539 + }
  540 + }
  541 +};
  542 +</script>
ruoyi-ui/src/views/system/sms/smsLog.vue 0 → 100644
  1 +<template>
  2 + <div class="app-container">
  3 +
  4 + <!-- 搜索工作栏 -->
  5 + <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
  6 + <el-form-item label="手机号" prop="mobile">
  7 + <el-input v-model="queryParams.mobile" placeholder="请输入手机号" clearable size="small" @keyup.enter.native="handleQuery"/>
  8 + </el-form-item>
  9 + <el-form-item label="短信渠道" prop="channelId">
  10 + <el-select v-model="queryParams.channelId" placeholder="请选择短信渠道" clearable size="small">
  11 + <el-option v-for="channel in channelOptions"
  12 + :key="channel.id" :value="channel.id"
  13 + :label="channel.signature + '【' + getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, channel.code) + '】'" />
  14 + </el-select>
  15 + </el-form-item>
  16 + <el-form-item label="模板编号" prop="templateId">
  17 + <el-input v-model="queryParams.templateId" placeholder="请输入模板编号" clearable size="small" @keyup.enter.native="handleQuery"/>
  18 + </el-form-item>
  19 + <el-form-item label="发送状态" prop="sendStatus">
  20 + <el-select v-model="queryParams.sendStatus" placeholder="请选择发送状态" clearable size="small">
  21 + <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_SEND_STATUS)"
  22 + :key="dict.value" :label="dict.label" :value="dict.value"/>
  23 + </el-select>
  24 + </el-form-item>
  25 + <el-form-item label="发送时间">
  26 + <el-date-picker v-model="dateRangeSendTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
  27 + type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
  28 + </el-form-item>
  29 + <el-form-item label="接收状态" prop="receiveStatus">
  30 + <el-select v-model="queryParams.receiveStatus" placeholder="请选择接收状态" clearable size="small">
  31 + <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_RECEIVE_STATUS)"
  32 + :key="dict.value" :label="dict.label" :value="dict.value"/>
  33 + </el-select>
  34 + </el-form-item>
  35 + <el-form-item label="接收时间">
  36 + <el-date-picker v-model="dateRangeReceiveTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
  37 + type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
  38 + </el-form-item>
  39 + <el-form-item>
  40 + <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  41 + <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  42 + </el-form-item>
  43 + </el-form>
  44 +
  45 + <!-- 操作工具栏 -->
  46 + <el-row :gutter="10" class="mb8">
  47 + <el-col :span="1.5">
  48 + <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
  49 + v-hasPermi="['system:sms-log:create']">新增</el-button>
  50 + </el-col>
  51 + <el-col :span="1.5">
  52 + <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
  53 + v-hasPermi="['system:sms-log:export']">导出</el-button>
  54 + </el-col>
  55 + <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  56 + </el-row>
  57 +
  58 + <!-- 列表 -->
  59 + <el-table v-loading="loading" :data="list">
  60 + <el-table-column label="编号" align="center" prop="id" />
  61 + <el-table-column label="创建时间" align="center" prop="createTime" width="180">
  62 + <template slot-scope="scope">
  63 + <span>{{ parseTime(scope.row.createTime) }}</span>
  64 + </template>
  65 + </el-table-column>
  66 + <el-table-column label="手机号" align="center" prop="mobile" width="120">
  67 + <template slot-scope="scope">
  68 + <div>{{ scope.row.mobile }}</div>
  69 + <div v-if="scope.row.userType && scope.row.userId">
  70 + {{ getDictDataLabel(DICT_TYPE.USER_TYPE, scope.row.userType) + '(' + scope.row.userId + ')' }}
  71 + </div>
  72 + </template>
  73 + </el-table-column>
  74 + <el-table-column label="短信内容" align="center" prop="templateContent" width="300" />
  75 + <el-table-column label="发送状态" align="center" width="180">
  76 + <template slot-scope="scope">
  77 + <div>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_SEND_STATUS, scope.row.sendStatus) }}</div>
  78 + <div>{{ parseTime(scope.row.sendTime) }}</div>
  79 + </template>
  80 + </el-table-column>
  81 + <el-table-column label="接收状态" align="center" width="180">
  82 + <template slot-scope="scope">
  83 + <div>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_RECEIVE_STATUS, scope.row.receiveStatus) }}</div>
  84 + <div>{{ parseTime(scope.row.receiveTime) }}</div>
  85 + </template>
  86 + </el-table-column>
  87 + <el-table-column label="短信渠道" align="center" width="120">
  88 + <template slot-scope="scope">
  89 + <div>{{ formatChannelSignature(scope.row.channelId) }}</div>
  90 + <div>【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}】</div>
  91 + </template>
  92 + </el-table-column>
  93 + <el-table-column label="模板编号" align="center" prop="templateId" />
  94 + <el-table-column label="短信类型" align="center" prop="templateType">
  95 + <template slot-scope="scope">
  96 + <span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, scope.row.templateType) }}</span>
  97 + </template>
  98 + </el-table-column>
  99 + <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  100 + <template slot-scope="scope">
  101 + <el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row,scope.index)"
  102 + v-hasPermi="['system:sms-log:query']">详细</el-button>
  103 + </template>
  104 + </el-table-column>
  105 + </el-table>
  106 + <!-- 分页组件 -->
  107 + <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
  108 + @pagination="getList"/>
  109 +
  110 + <!-- 短信日志详细 -->
  111 + <el-dialog title="短信日志详细" :visible.sync="open" width="700px" append-to-body>
  112 + <el-form ref="form" :model="form" label-width="140px" size="mini">
  113 + <el-row>
  114 + <el-col :span="24">
  115 + <el-form-item label="日志主键:">{{ form.id }}</el-form-item>
  116 + </el-col>
  117 + <el-col :span="24">
  118 + <el-form-item label="短信渠道:">
  119 + {{ formatChannelSignature(form.channelId) }}【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, form.channelCode) }}】
  120 + </el-form-item>
  121 + </el-col>
  122 + <el-col :span="24">
  123 + <el-form-item label="短信模板:">
  124 + {{ form.templateId }} | {{ form.templateCode}} | {{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, form.templateType) }}
  125 + </el-form-item>
  126 + </el-col>
  127 + <el-col :span="24">
  128 + <el-form-item label="API 的模板编号:">{{ form.apiTemplateId }}</el-form-item>
  129 + </el-col>
  130 + <el-col :span="24">
  131 + <el-form-item label="用户信息:">{{ form.mobile }}
  132 + <span v-if="form.userType && form.userId"> | {{ getDictDataLabel(DICT_TYPE.USER_TYPE, form.userType) }} | {{ form.userId }}</span>
  133 + </el-form-item>
  134 + </el-col>
  135 + <el-col :span="24">
  136 + <el-form-item label="短信内容:">{{ form.templateContent }}</el-form-item>
  137 + </el-col>
  138 + <el-col :span="24">
  139 + <el-form-item label="短信参数:">{{ form.templateParams }}</el-form-item>
  140 + </el-col>
  141 + <el-col :span="24">
  142 + <el-form-item label="创建时间:">{{ parseTime(form.createTime) }}</el-form-item>
  143 + </el-col>
  144 + <el-col :span="24">
  145 + <el-form-item label="发送状态:">{{ getDictDataLabel(DICT_TYPE.SYS_SMS_SEND_STATUS, form.sendStatus) }}</el-form-item>
  146 + </el-col>
  147 + <el-col :span="24">
  148 + <el-form-item label="发送时间:">{{ parseTime(form.sendTime) }}</el-form-item>
  149 + </el-col>
  150 + <el-col :span="24">
  151 + <el-form-item label="发送结果:">{{ form.sendCode }} | {{ form.sendMsg }}
  152 + </el-form-item>
  153 + </el-col>
  154 + <el-col :span="24">
  155 + <el-form-item label="API 发送结果:">{{ form.apiSendCode }} | {{ form.apiSendMsg }}</el-form-item>
  156 + </el-col>
  157 + <el-col :span="24">
  158 + <el-form-item label="API 短信编号:">{{ form.apiSerialNo }}</el-form-item>
  159 + </el-col>
  160 + <el-col :span="24">
  161 + <el-form-item label="API 请求编号:">{{ form.apiRequestId }}</el-form-item>
  162 + </el-col>
  163 + <el-col :span="24">
  164 + <el-form-item label="接收状态:">{{ getDictDataLabel(DICT_TYPE.SYS_SMS_RECEIVE_STATUS, form.receiveStatus) }}</el-form-item>
  165 + </el-col>
  166 + <el-col :span="24">
  167 + <el-form-item label="接收时间:">{{ parseTime(form.receiveTime) }}</el-form-item>
  168 + </el-col>
  169 + <el-col :span="24">
  170 + <el-form-item label="API 接收结果:">{{ form.apiReceiveCode }} | {{ form.apiReceiveMsg }}
  171 + </el-form-item>
  172 + </el-col>
  173 + </el-row>
  174 + </el-form>
  175 + <div slot="footer" class="dialog-footer">
  176 + <el-button @click="open = false">关 闭</el-button>
  177 + </div>
  178 + </el-dialog>
  179 +
  180 + </div>
  181 +</template>
  182 +
  183 +<script>
  184 +import { getSmsLogPage, exportSmsLogExcel } from "@/api/system/sms/smsLog";
  185 +import { getSimpleSmsChannels } from "@/api/system/sms/smsChannel";
  186 +
  187 +export default {
  188 + name: "SmsLog",
  189 + components: {
  190 + },
  191 + data() {
  192 + return {
  193 + // 遮罩层
  194 + loading: true,
  195 + // 显示搜索条件
  196 + showSearch: true,
  197 + // 总条数
  198 + total: 0,
  199 + // 短信日志列表
  200 + list: [],
  201 + // 弹出层标题
  202 + title: "",
  203 + // 是否显示弹出层
  204 + open: false,
  205 + dateRangeSendTime: [],
  206 + dateRangeReceiveTime: [],
  207 + // 表单参数
  208 + form: {},
  209 + // 查询参数
  210 + queryParams: {
  211 + pageNo: 1,
  212 + pageSize: 10,
  213 + channelId: null,
  214 + templateId: null,
  215 + mobile: null,
  216 + sendStatus: null,
  217 + receiveStatus: null,
  218 + },
  219 + // 短信渠道
  220 + channelOptions: [],
  221 + };
  222 + },
  223 + created() {
  224 + this.getList();
  225 + // 获得短信渠道
  226 + getSimpleSmsChannels().then(response => {
  227 + this.channelOptions = response.data;
  228 + })
  229 + },
  230 + methods: {
  231 + /** 查询列表 */
  232 + getList() {
  233 + this.loading = true;
  234 + // 处理查询参数
  235 + let params = {...this.queryParams};
  236 + this.addBeginAndEndTime(params, this.dateRangeSendTime, 'sendTime');
  237 + this.addBeginAndEndTime(params, this.dateRangeReceiveTime, 'receiveTime');
  238 + // 执行查询
  239 + getSmsLogPage(params).then(response => {
  240 + this.list = response.data.list;
  241 + this.total = response.data.total;
  242 + this.loading = false;
  243 + });
  244 + },
  245 + /** 取消按钮 */
  246 + cancel() {
  247 + this.open = false;
  248 + this.reset();
  249 + },
  250 + /** 搜索按钮操作 */
  251 + handleQuery() {
  252 + this.queryParams.pageNo = 1;
  253 + this.getList();
  254 + },
  255 + /** 重置按钮操作 */
  256 + resetQuery() {
  257 + this.dateRangeSendTime = [];
  258 + this.dateRangeReceiveTime = [];
  259 + this.resetForm("queryForm");
  260 + this.handleQuery();
  261 + },
  262 + /** 导出按钮操作 */
  263 + handleExport() {
  264 + // 处理查询参数
  265 + let params = {...this.queryParams};
  266 + params.pageNo = undefined;
  267 + params.pageSize = undefined;
  268 + this.addBeginAndEndTime(params, this.dateRangeSendTime, 'sendTime');
  269 + this.addBeginAndEndTime(params, this.dateRangeReceiveTime, 'receiveTime');
  270 + // 执行导出
  271 + this.$confirm('是否确认导出所有短信日志数据项?', "警告", {
  272 + confirmButtonText: "确定",
  273 + cancelButtonText: "取消",
  274 + type: "warning"
  275 + }).then(function() {
  276 + return exportSmsLogExcel(params);
  277 + }).then(response => {
  278 + this.downloadExcel(response, '短信日志.xls');
  279 + })
  280 + },
  281 + /** 详细按钮操作 */
  282 + handleView(row) {
  283 + this.open = true;
  284 + this.form = row;
  285 + },
  286 + /** 格式化短信渠道 */
  287 + formatChannelSignature(channelId) {
  288 + for (const channel of this.channelOptions) {
  289 + if (channel.id === channelId) {
  290 + return channel.signature;
  291 + }
  292 + }
  293 + return '找不到签名:' + channelId;
  294 + }
  295 + }
  296 +};
  297 +</script>
ruoyi-ui/src/views/system/sms/smsTemplate.vue 0 → 100644
  1 +<template>
  2 + <div class="app-container">
  3 +
  4 + <!-- 搜索工作栏 -->
  5 + <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="150px">
  6 + <el-form-item label="短信类型" prop="type">
  7 + <el-select v-model="queryParams.type" placeholder="请选择短信类型" clearable size="small">
  8 + <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE)"
  9 + :key="dict.value" :label="dict.label" :value="dict.value"/>
  10 + </el-select>
  11 + </el-form-item>
  12 + <el-form-item label="开启状态" prop="status">
  13 + <el-select v-model="queryParams.status" placeholder="请选择开启状态" clearable size="small">
  14 + <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
  15 + :key="dict.value" :label="dict.label" :value="dict.value"/>
  16 + </el-select>
  17 + </el-form-item>
  18 + <el-form-item label="模板编码" prop="code">
  19 + <el-input v-model="queryParams.code" placeholder="请输入模板编码" clearable size="small" @keyup.enter.native="handleQuery"/>
  20 + </el-form-item>
  21 + <el-form-item label="短信 API 的模板编号" prop="apiTemplateId">
  22 + <el-input v-model="queryParams.apiTemplateId" placeholder="请输入短信 API 的模板编号" clearable size="small" @keyup.enter.native="handleQuery"/>
  23 + </el-form-item>
  24 + <el-form-item label="短信渠道" prop="channelId">
  25 + <el-select v-model="queryParams.channelId" placeholder="请选择短信渠道" clearable size="small">
  26 + <el-option v-for="channel in channelOptions"
  27 + :key="channel.id" :value="channel.id"
  28 + :label="channel.signature + '【' + getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, channel.code) + '】'" />
  29 + </el-select>
  30 + </el-form-item>
  31 + <el-form-item label="创建时间">
  32 + <el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
  33 + type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
  34 + </el-form-item>
  35 + <el-form-item>
  36 + <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  37 + <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  38 + </el-form-item>
  39 + </el-form>
  40 +
  41 + <!-- 操作工具栏 -->
  42 + <el-row :gutter="10" class="mb8">
  43 + <el-col :span="1.5">
  44 + <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
  45 + v-hasPermi="['system:sms-template:create']">新增</el-button>
  46 + </el-col>
  47 + <el-col :span="1.5">
  48 + <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
  49 + v-hasPermi="['system:sms-template:export']">导出</el-button>
  50 + </el-col>
  51 + <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  52 + </el-row>
  53 +
  54 + <!-- 列表 -->
  55 + <el-table v-loading="loading" :data="list">
  56 + <el-table-column label="模板编码" align="center" prop="code" />
  57 + <el-table-column label="模板名称" align="center" prop="name" />
  58 + <el-table-column label="模板内容" align="center" prop="content" width="300" />
  59 + <el-table-column label="短信类型" align="center" prop="type">
  60 + <template slot-scope="scope">
  61 + <span>{{ getDictDataLabel(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE, scope.row.type) }}</span>
  62 + </template>
  63 + </el-table-column>
  64 + <el-table-column label="开启状态" align="center" prop="status">
  65 + <template slot-scope="scope">
  66 + <span>{{ getDictDataLabel(DICT_TYPE.SYS_COMMON_STATUS, scope.row.status) }}</span>
  67 + </template>
  68 + </el-table-column>
  69 + <el-table-column label="备注" align="center" prop="remark" />
  70 + <el-table-column label="短信 API 的模板编号" align="center" prop="apiTemplateId" width="180" />
  71 + <el-table-column label="短信渠道" align="center" width="120">
  72 + <template slot-scope="scope">
  73 + <div>{{ formatChannelSignature(scope.row.channelId) }}</div>
  74 + <div>【{{ getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, scope.row.channelCode) }}】</div>
  75 + </template>
  76 + </el-table-column>
  77 + <el-table-column label="创建时间" align="center" prop="createTime" width="180">
  78 + <template slot-scope="scope">
  79 + <span>{{ parseTime(scope.row.createTime) }}</span>
  80 + </template>
  81 + </el-table-column>
  82 + <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
  83 + <template slot-scope="scope">
  84 + <el-button size="mini" type="text" icon="el-icon-share" @click="handleSendSms(scope.row)"
  85 + v-hasPermi="['system:sms-template:send-sms']">测试</el-button>
  86 + <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
  87 + v-hasPermi="['system:sms-template:update']">修改</el-button>
  88 + <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
  89 + v-hasPermi="['system:sms-template:delete']">删除</el-button>
  90 + </template>
  91 + </el-table-column>
  92 + </el-table>
  93 + <!-- 分页组件 -->
  94 + <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
  95 + @pagination="getList"/>
  96 +
  97 + <!-- 对话框(添加 / 修改) -->
  98 + <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
  99 + <el-form ref="form" :model="form" :rules="rules" label-width="140px">
  100 + <el-form-item label="短信渠道编号" prop="channelId">
  101 + <el-select v-model="form.channelId" placeholder="请选择短信渠道编号">
  102 + <el-option v-for="channel in channelOptions"
  103 + :key="channel.id" :value="channel.id"
  104 + :label="channel.signature + '【' + getDictDataLabel(DICT_TYPE.SYS_SMS_CHANNEL_CODE, channel.code) + '】'" />
  105 + </el-select>
  106 + </el-form-item>
  107 + <el-form-item label="短信类型" prop="type">
  108 + <el-select v-model="form.type" placeholder="请选择短信类型">
  109 + <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYS_SMS_TEMPLATE_TYPE)"
  110 + :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
  111 + </el-select>
  112 + </el-form-item>
  113 + <el-form-item label="模板编号" prop="code">
  114 + <el-input v-model="form.code" placeholder="请输入模板编号" />
  115 + </el-form-item>
  116 + <el-form-item label="模板名称" prop="name">
  117 + <el-input v-model="form.name" placeholder="请输入模板名称" />
  118 + </el-form-item>
  119 + <el-form-item label="模板内容" prop="content">
  120 + <el-input type="textarea" v-model="form.content" placeholder="请输入模板内容" />
  121 + </el-form-item>
  122 + <el-form-item label="开启状态" prop="status">
  123 + <el-radio-group v-model="form.status">
  124 + <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.SYS_COMMON_STATUS)"
  125 + :key="dict.value" :label="parseInt(dict.value)">{{dict.label}}</el-radio>
  126 + </el-radio-group>
  127 + </el-form-item>
  128 + <el-form-item label="短信 API 模板编号" prop="apiTemplateId">
  129 + <el-input v-model="form.apiTemplateId" placeholder="请输入短信 API 的模板编号" />
  130 + </el-form-item>
  131 + <el-form-item label="备注" prop="remark">
  132 + <el-input v-model="form.remark" placeholder="请输入备注" />
  133 + </el-form-item>
  134 + </el-form>
  135 + <div slot="footer" class="dialog-footer">
  136 + <el-button type="primary" @click="submitForm">确 定</el-button>
  137 + <el-button @click="cancel">取 消</el-button>
  138 + </div>
  139 + </el-dialog>
  140 +
  141 + <!-- 对话框(发送短信) -->
  142 + <el-dialog title="测试发送短信" :visible.sync="sendSmsOpen" width="500px" append-to-body>
  143 + <el-form ref="sendSmsForm" :model="sendSmsForm" :rules="sendSmsRules" label-width="140px">
  144 + <el-form-item label="模板内容" prop="content">
  145 + <el-input v-model="sendSmsForm.content" type="textarea" placeholder="请输入模板内容" readonly />
  146 + </el-form-item>
  147 + <el-form-item label="手机号" prop="mobile">
  148 + <el-input v-model="sendSmsForm.mobile" placeholder="请输入手机号" />
  149 + </el-form-item>
  150 + <el-form-item v-for="param in sendSmsForm.params" :label="'参数 {' + param + '}'" :prop="'templateParams.' + param">
  151 + <el-input v-model="sendSmsForm.templateParams[param]" :placeholder="'请输入 ' + param + ' 参数'" />
  152 + </el-form-item>
  153 + </el-form>
  154 + <div slot="footer" class="dialog-footer">
  155 + <el-button type="primary" @click="submitSendSmsForm">确 定</el-button>
  156 + <el-button @click="cancelSendSms">取 消</el-button>
  157 + </div>
  158 + </el-dialog>
  159 +
  160 + </div>
  161 +</template>
  162 +
  163 +<script>
  164 +import { createSmsTemplate, updateSmsTemplate, deleteSmsTemplate, getSmsTemplate, getSmsTemplatePage,
  165 + exportSmsTemplateExcel, sendSms } from "@/api/system/sms/smsTemplate";
  166 +import { getSimpleSmsChannels } from "@/api/system/sms/smsChannel";
  167 +
  168 +export default {
  169 + name: "SmsTemplate",
  170 + data() {
  171 + return {
  172 + // 遮罩层
  173 + loading: true,
  174 + // 显示搜索条件
  175 + showSearch: true,
  176 + // 总条数
  177 + total: 0,
  178 + // 短信模板列表
  179 + list: [],
  180 + // 弹出层标题
  181 + title: "",
  182 + // 是否显示弹出层
  183 + open: false,
  184 + dateRangeCreateTime: [],
  185 + // 查询参数
  186 + queryParams: {
  187 + pageNo: 1,
  188 + pageSize: 10,
  189 + type: null,
  190 + status: null,
  191 + code: null,
  192 + content: null,
  193 + apiTemplateId: null,
  194 + channelId: null,
  195 + },
  196 + // 表单参数
  197 + form: {},
  198 + // 表单校验
  199 + rules: {
  200 + type: [{ required: true, message: "短信类型不能为空", trigger: "change" }],
  201 + status: [{ required: true, message: "开启状态不能为空", trigger: "blur" }],
  202 + code: [{ required: true, message: "模板编码不能为空", trigger: "blur" }],
  203 + name: [{ required: true, message: "模板名称不能为空", trigger: "blur" }],
  204 + content: [{ required: true, message: "模板内容不能为空", trigger: "blur" }],
  205 + apiTemplateId: [{ required: true, message: "短信 API 的模板编号不能为空", trigger: "blur" }],
  206 + channelId: [{ required: true, message: "短信渠道编号不能为空", trigger: "change" }],
  207 + },
  208 + // 短信渠道
  209 + channelOptions: [],
  210 + // 发送短信
  211 + sendSmsOpen: false,
  212 + sendSmsForm: {
  213 + params: [], // 模板的参数列表
  214 + },
  215 + sendSmsRules: {
  216 + mobile: [{ required: true, message: "手机不能为空", trigger: "blur" }],
  217 + templateCode: [{ required: true, message: "手机不能为空", trigger: "blur" }],
  218 + templateParams: { }
  219 + }
  220 + };
  221 + },
  222 + created() {
  223 + this.getList();
  224 + // 获得短信渠道
  225 + getSimpleSmsChannels().then(response => {
  226 + this.channelOptions = response.data;
  227 + })
  228 + },
  229 + methods: {
  230 + /** 查询列表 */
  231 + getList() {
  232 + this.loading = true;
  233 + // 处理查询参数
  234 + let params = {...this.queryParams};
  235 + this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
  236 + // 执行查询
  237 + getSmsTemplatePage(params).then(response => {
  238 + this.list = response.data.list;
  239 + this.total = response.data.total;
  240 + this.loading = false;
  241 + });
  242 + },
  243 + /** 取消按钮 */
  244 + cancel() {
  245 + this.open = false;
  246 + this.reset();
  247 + },
  248 + /** 表单重置 */
  249 + reset() {
  250 + this.form = {
  251 + id: undefined,
  252 + type: undefined,
  253 + status: undefined,
  254 + code: undefined,
  255 + name: undefined,
  256 + content: undefined,
  257 + remark: undefined,
  258 + apiTemplateId: undefined,
  259 + channelId: undefined,
  260 + };
  261 + this.resetForm("form");
  262 + },
  263 + /** 搜索按钮操作 */
  264 + handleQuery() {
  265 + this.queryParams.pageNo = 1;
  266 + this.getList();
  267 + },
  268 + /** 重置按钮操作 */
  269 + resetQuery() {
  270 + this.dateRangeCreateTime = [];
  271 + this.resetForm("queryForm");
  272 + this.handleQuery();
  273 + },
  274 + /** 新增按钮操作 */
  275 + handleAdd() {
  276 + this.reset();
  277 + this.open = true;
  278 + this.title = "添加短信模板";
  279 + },
  280 + /** 修改按钮操作 */
  281 + handleUpdate(row) {
  282 + this.reset();
  283 + const id = row.id;
  284 + getSmsTemplate(id).then(response => {
  285 + this.form = response.data;
  286 + this.open = true;
  287 + this.title = "修改短信模板";
  288 + });
  289 + },
  290 + /** 提交按钮 */
  291 + submitForm() {
  292 + this.$refs["form"].validate(valid => {
  293 + if (!valid) {
  294 + return;
  295 + }
  296 + // 修改的提交
  297 + if (this.form.id != null) {
  298 + updateSmsTemplate(this.form).then(response => {
  299 + this.msgSuccess("修改成功");
  300 + this.open = false;
  301 + this.getList();
  302 + });
  303 + return;
  304 + }
  305 + // 添加的提交
  306 + createSmsTemplate(this.form).then(response => {
  307 + this.msgSuccess("新增成功");
  308 + this.open = false;
  309 + this.getList();
  310 + });
  311 + });
  312 + },
  313 + /** 删除按钮操作 */
  314 + handleDelete(row) {
  315 + const id = row.id;
  316 + this.$confirm('是否确认删除短信模板编号为"' + id + '"的数据项?', "警告", {
  317 + confirmButtonText: "确定",
  318 + cancelButtonText: "取消",
  319 + type: "warning"
  320 + }).then(function() {
  321 + return deleteSmsTemplate(id);
  322 + }).then(() => {
  323 + this.getList();
  324 + this.msgSuccess("删除成功");
  325 + })
  326 + },
  327 + /** 导出按钮操作 */
  328 + handleExport() {
  329 + // 处理查询参数
  330 + let params = {...this.queryParams};
  331 + params.pageNo = undefined;
  332 + params.pageSize = undefined;
  333 + this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
  334 + // 执行导出
  335 + this.$confirm('是否确认导出所有短信模板数据项?', "警告", {
  336 + confirmButtonText: "确定",
  337 + cancelButtonText: "取消",
  338 + type: "warning"
  339 + }).then(function() {
  340 + return exportSmsTemplateExcel(params);
  341 + }).then(response => {
  342 + this.downloadExcel(response, '短信模板.xls');
  343 + })
  344 + },
  345 + /** 发送短息按钮 */
  346 + handleSendSms(row) {
  347 + this.resetSendSms(row);
  348 + // 设置参数
  349 + this.sendSmsForm.content = row.content;
  350 + this.sendSmsForm.params = row.params;
  351 + this.sendSmsForm.templateCode = row.code;
  352 + this.sendSmsForm.templateParams = row.params.reduce(function(obj, item) {
  353 + obj[item] = undefined;
  354 + return obj;
  355 + }, {});
  356 + // 根据 row 重置 rules
  357 + this.sendSmsRules.templateParams = row.params.reduce(function(obj, item) {
  358 + obj[item] = { required: true, message: '参数 ' + item + " 不能为空", trigger: "change" };
  359 + return obj;
  360 + }, {});
  361 + // 设置打开
  362 + this.sendSmsOpen = true;
  363 + },
  364 + /** 重置发送短信的表单 */
  365 + resetSendSms() {
  366 + // 根据 row 重置表单
  367 + this.sendSmsForm = {
  368 + content: undefined,
  369 + params: undefined,
  370 + mobile: undefined,
  371 + templateCode: undefined,
  372 + templateParams: {}
  373 + };
  374 + this.resetForm("sendSmsForm");
  375 + },
  376 + /** 取消发送短信 */
  377 + cancelSendSms() {
  378 + this.sendSmsOpen = false;
  379 + this.resetSendSms();
  380 + },
  381 + /** 提交按钮 */
  382 + submitSendSmsForm() {
  383 + this.$refs["sendSmsForm"].validate(valid => {
  384 + if (!valid) {
  385 + return;
  386 + }
  387 + // 添加的提交
  388 + sendSms(this.sendSmsForm).then(response => {
  389 + this.msgSuccess("提交发送成功!发送结果,见发送日志编号:" + response.data);
  390 + this.sendSmsOpen = false;
  391 + });
  392 + });
  393 + },
  394 + /** 格式化短信渠道 */
  395 + formatChannelSignature(channelId) {
  396 + for (const channel of this.channelOptions) {
  397 + if (channel.id === channelId) {
  398 + return channel.signature;
  399 + }
  400 + }
  401 + return '找不到签名:' + channelId;
  402 + }
  403 + }
  404 +};
  405 +</script>
sql/ruoyi-vue-pro.sql
1 /* 1 /*
2 Navicat Premium Data Transfer 2 Navicat Premium Data Transfer
3 3
4 - Source Server : 127.0.0.1 4 + Source Server : local-mysql001
5 Source Server Type : MySQL 5 Source Server Type : MySQL
6 Source Server Version : 50718 6 Source Server Version : 50718
7 Source Host : localhost:3306 7 Source Host : localhost:3306
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 Target Server Version : 50718 11 Target Server Version : 50718
12 File Encoding : 65001 12 File Encoding : 65001
13 13
14 - Date: 21/03/2021 18:53:24 14 + Date: 05/04/2021 23:51:38
15 */ 15 */
16 16
17 SET NAMES utf8mb4; 17 SET NAMES utf8mb4;
@@ -43,7 +43,7 @@ CREATE TABLE `inf_api_access_log` ( @@ -43,7 +43,7 @@ CREATE TABLE `inf_api_access_log` (
43 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', 43 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
44 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 44 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
45 PRIMARY KEY (`id`) USING BTREE 45 PRIMARY KEY (`id`) USING BTREE
46 -) ENGINE=InnoDB AUTO_INCREMENT=1318 DEFAULT CHARSET=utf8mb4 COMMENT='API 访问日志表'; 46 +) ENGINE=InnoDB AUTO_INCREMENT=1909 DEFAULT CHARSET=utf8mb4 COMMENT='API 访问日志表';
47 47
48 -- ---------------------------- 48 -- ----------------------------
49 -- Records of inf_api_access_log 49 -- Records of inf_api_access_log
@@ -84,7 +84,7 @@ CREATE TABLE `inf_api_error_log` ( @@ -84,7 +84,7 @@ CREATE TABLE `inf_api_error_log` (
84 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', 84 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
85 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 85 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
86 PRIMARY KEY (`id`) USING BTREE 86 PRIMARY KEY (`id`) USING BTREE
87 -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='系统异常日志'; 87 +) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='系统异常日志';
88 88
89 -- ---------------------------- 89 -- ----------------------------
90 -- Records of inf_api_error_log 90 -- Records of inf_api_error_log
@@ -201,7 +201,7 @@ CREATE TABLE `inf_job_log` ( @@ -201,7 +201,7 @@ CREATE TABLE `inf_job_log` (
201 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', 201 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
202 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 202 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
203 PRIMARY KEY (`id`) USING BTREE 203 PRIMARY KEY (`id`) USING BTREE
204 -) ENGINE=InnoDB AUTO_INCREMENT=627 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务日志表'; 204 +) ENGINE=InnoDB AUTO_INCREMENT=1035 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务日志表';
205 205
206 -- ---------------------------- 206 -- ----------------------------
207 -- Records of inf_job_log 207 -- Records of inf_job_log
@@ -264,7 +264,7 @@ CREATE TABLE `sys_dict_data` ( @@ -264,7 +264,7 @@ CREATE TABLE `sys_dict_data` (
264 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', 264 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
265 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 265 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
266 PRIMARY KEY (`id`) USING BTREE 266 PRIMARY KEY (`id`) USING BTREE
267 -) ENGINE=InnoDB AUTO_INCREMENT=65 DEFAULT CHARSET=utf8mb4 COMMENT='字典数æ®è¡¨'; 267 +) ENGINE=InnoDB AUTO_INCREMENT=70 DEFAULT CHARSET=utf8mb4 COMMENT='字典数æ®è¡¨';
268 268
269 -- ---------------------------- 269 -- ----------------------------
270 -- Records of sys_dict_data 270 -- Records of sys_dict_data
@@ -324,6 +324,11 @@ INSERT INTO `sys_dict_data` VALUES (61, 2, &#39;管ç†å‘˜&#39;, &#39;2&#39;, &#39;user_type&#39;, 0, NUL @@ -324,6 +324,11 @@ INSERT INTO `sys_dict_data` VALUES (61, 2, &#39;管ç†å‘˜&#39;, &#39;2&#39;, &#39;user_type&#39;, 0, NUL
324 INSERT INTO `sys_dict_data` VALUES (62, 0, '未处ç†', '0', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:19', '', '2021-02-26 08:11:23', b'0'); 324 INSERT INTO `sys_dict_data` VALUES (62, 0, '未处ç†', '0', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:19', '', '2021-02-26 08:11:23', b'0');
325 INSERT INTO `sys_dict_data` VALUES (63, 1, '已处ç†', '1', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:26', '', '2021-02-26 08:11:29', b'0'); 325 INSERT INTO `sys_dict_data` VALUES (63, 1, '已处ç†', '1', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:26', '', '2021-02-26 08:11:29', b'0');
326 INSERT INTO `sys_dict_data` VALUES (64, 2, '已忽略', '2', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:34', '', '2021-02-26 08:11:34', b'0'); 326 INSERT INTO `sys_dict_data` VALUES (64, 2, '已忽略', '2', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:34', '', '2021-02-26 08:11:34', b'0');
  327 +INSERT INTO `sys_dict_data` VALUES (65, 0, '云片', 'YUN_PIAN', 'sys_sms_channel_code', 0, NULL, '1', '2021-04-05 01:05:14', '1', '2021-04-05 01:05:14', b'0');
  328 +INSERT INTO `sys_dict_data` VALUES (66, 0, '阿里云', 'ALIYUN', 'sys_sms_channel_code', 0, NULL, '1', '2021-04-05 01:05:26', '1', '2021-04-05 01:05:26', b'0');
  329 +INSERT INTO `sys_dict_data` VALUES (67, 1, '验è¯ç ', '1', 'sys_sms_template_type', 0, NULL, '1', '2021-04-05 21:50:57', '1', '2021-04-05 21:50:57', b'0');
  330 +INSERT INTO `sys_dict_data` VALUES (68, 2, '通知', '2', 'sys_sms_template_type', 0, NULL, '1', '2021-04-05 21:51:08', '1', '2021-04-05 21:51:08', b'0');
  331 +INSERT INTO `sys_dict_data` VALUES (69, 0, 'è¥é”€', '3', 'sys_sms_template_type', 0, NULL, '1', '2021-04-05 21:51:15', '1', '2021-04-05 21:51:15', b'0');
327 COMMIT; 332 COMMIT;
328 333
329 -- ---------------------------- 334 -- ----------------------------
@@ -343,7 +348,7 @@ CREATE TABLE `sys_dict_type` ( @@ -343,7 +348,7 @@ CREATE TABLE `sys_dict_type` (
343 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 348 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
344 PRIMARY KEY (`id`) USING BTREE, 349 PRIMARY KEY (`id`) USING BTREE,
345 UNIQUE KEY `dict_type` (`type`) 350 UNIQUE KEY `dict_type` (`type`)
346 -) ENGINE=InnoDB AUTO_INCREMENT=111 DEFAULT CHARSET=utf8mb4 COMMENT='字典类型表'; 351 +) ENGINE=InnoDB AUTO_INCREMENT=113 DEFAULT CHARSET=utf8mb4 COMMENT='字典类型表';
347 352
348 -- ---------------------------- 353 -- ----------------------------
349 -- Records of sys_dict_type 354 -- Records of sys_dict_type
@@ -366,6 +371,8 @@ INSERT INTO `sys_dict_type` VALUES (107, &#39;定时任务状æ€&#39;, &#39;inf_job_status&#39;, @@ -366,6 +371,8 @@ INSERT INTO `sys_dict_type` VALUES (107, &#39;定时任务状æ€&#39;, &#39;inf_job_status&#39;,
366 INSERT INTO `sys_dict_type` VALUES (108, '定时任务日志状æ€', 'inf_job_log_status', 0, NULL, '', '2021-02-08 10:03:51', '', '2021-02-08 10:03:51', b'0'); 371 INSERT INTO `sys_dict_type` VALUES (108, '定时任务日志状æ€', 'inf_job_log_status', 0, NULL, '', '2021-02-08 10:03:51', '', '2021-02-08 10:03:51', b'0');
367 INSERT INTO `sys_dict_type` VALUES (109, '用户类型', 'user_type', 0, NULL, '', '2021-02-26 00:15:51', '', '2021-02-26 00:15:51', b'0'); 372 INSERT INTO `sys_dict_type` VALUES (109, '用户类型', 'user_type', 0, NULL, '', '2021-02-26 00:15:51', '', '2021-02-26 00:15:51', b'0');
368 INSERT INTO `sys_dict_type` VALUES (110, 'API 异常数æ®çš„处ç†çжæ€', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:01', '', '2021-02-26 07:07:01', b'0'); 373 INSERT INTO `sys_dict_type` VALUES (110, 'API 异常数æ®çš„处ç†çжæ€', 'inf_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:01', '', '2021-02-26 07:07:01', b'0');
  374 +INSERT INTO `sys_dict_type` VALUES (111, '短信渠é“ç¼–ç ', 'sys_sms_channel_code', 0, NULL, '1', '2021-04-05 01:04:50', '1', '2021-04-05 01:04:50', b'0');
  375 +INSERT INTO `sys_dict_type` VALUES (112, '短信模æ¿çš„类型', 'sys_sms_template_type', 0, NULL, '1', '2021-04-05 21:50:43', '1', '2021-04-05 21:50:43', b'0');
369 COMMIT; 376 COMMIT;
370 377
371 -- ---------------------------- 378 -- ----------------------------
@@ -386,7 +393,7 @@ CREATE TABLE `sys_login_log` ( @@ -386,7 +393,7 @@ CREATE TABLE `sys_login_log` (
386 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', 393 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
387 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 394 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
388 PRIMARY KEY (`id`) USING BTREE 395 PRIMARY KEY (`id`) USING BTREE
389 -) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COMMENT='系统访问记录'; 396 +) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COMMENT='系统访问记录';
390 397
391 -- ---------------------------- 398 -- ----------------------------
392 -- Records of sys_login_log 399 -- Records of sys_login_log
@@ -415,7 +422,7 @@ CREATE TABLE `sys_menu` ( @@ -415,7 +422,7 @@ CREATE TABLE `sys_menu` (
415 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', 422 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
416 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 423 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
417 PRIMARY KEY (`id`) USING BTREE 424 PRIMARY KEY (`id`) USING BTREE
418 -) ENGINE=InnoDB AUTO_INCREMENT=1093 DEFAULT CHARSET=utf8mb4 COMMENT='èœå•æƒé™è¡¨'; 425 +) ENGINE=InnoDB AUTO_INCREMENT=1100 DEFAULT CHARSET=utf8mb4 COMMENT='èœå•æƒé™è¡¨';
419 426
420 -- ---------------------------- 427 -- ----------------------------
421 -- Records of sys_menu 428 -- Records of sys_menu
@@ -530,6 +537,12 @@ INSERT INTO `sys_menu` VALUES (1089, &#39;日志查询&#39;, &#39;infra:api-error-log:query&#39; @@ -530,6 +537,12 @@ INSERT INTO `sys_menu` VALUES (1089, &#39;日志查询&#39;, &#39;infra:api-error-log:query&#39;
530 INSERT INTO `sys_menu` VALUES (1090, '文件管ç†', '', 2, 0, 2, 'file', 'upload', 'infra/file/index', 0, '', '2021-03-12 20:16:20', '1', '2021-03-13 11:07:05', b'0'); 537 INSERT INTO `sys_menu` VALUES (1090, '文件管ç†', '', 2, 0, 2, 'file', 'upload', 'infra/file/index', 0, '', '2021-03-12 20:16:20', '1', '2021-03-13 11:07:05', b'0');
531 INSERT INTO `sys_menu` VALUES (1091, '文件查询', 'infra:file:query', 3, 1, 1090, '', '', '', 0, '', '2021-03-12 20:16:20', '', '2021-03-12 20:16:20', b'0'); 538 INSERT INTO `sys_menu` VALUES (1091, '文件查询', 'infra:file:query', 3, 1, 1090, '', '', '', 0, '', '2021-03-12 20:16:20', '', '2021-03-12 20:16:20', b'0');
532 INSERT INTO `sys_menu` VALUES (1092, '文件删除', 'infra:file:delete', 3, 4, 1090, '', '', '', 0, '', '2021-03-12 20:16:20', '', '2021-03-12 20:16:20', b'0'); 539 INSERT INTO `sys_menu` VALUES (1092, '文件删除', 'infra:file:delete', 3, 4, 1090, '', '', '', 0, '', '2021-03-12 20:16:20', '', '2021-03-12 20:16:20', b'0');
  540 +INSERT INTO `sys_menu` VALUES (1093, '短信管ç†', '', 1, 11, 1, 'sms', 'validCode', NULL, 0, '1', '2021-04-05 01:10:16', '1', '2021-04-05 01:11:38', b'0');
  541 +INSERT INTO `sys_menu` VALUES (1094, '短信渠é“', '', 2, 0, 1093, 'sms-channel', '', 'system/sms/smsChannel', 0, '', '2021-04-01 11:07:15', '1', '2021-04-05 20:32:53', b'0');
  542 +INSERT INTO `sys_menu` VALUES (1095, 'çŸ­ä¿¡æ¸ é“æŸ¥è¯¢', 'system:sms-channel:query', 3, 1, 1094, '', '', '', 0, '', '2021-04-01 11:07:15', '', '2021-04-01 11:07:15', b'0');
  543 +INSERT INTO `sys_menu` VALUES (1096, '短信渠é“创建', 'system:sms-channel:create', 3, 2, 1094, '', '', '', 0, '', '2021-04-01 11:07:15', '', '2021-04-01 11:07:15', b'0');
  544 +INSERT INTO `sys_menu` VALUES (1097, 'çŸ­ä¿¡æ¸ é“æ›´æ–°', 'system:sms-channel:update', 3, 3, 1094, '', '', '', 0, '', '2021-04-01 11:07:15', '', '2021-04-01 11:07:15', b'0');
  545 +INSERT INTO `sys_menu` VALUES (1098, '短信渠é“删除', 'system:sms-channel:delete', 3, 4, 1094, '', '', '', 0, '', '2021-04-01 11:07:15', '', '2021-04-01 11:07:15', b'0');
533 COMMIT; 546 COMMIT;
534 547
535 -- ---------------------------- 548 -- ----------------------------
@@ -589,7 +602,7 @@ CREATE TABLE `sys_operate_log` ( @@ -589,7 +602,7 @@ CREATE TABLE `sys_operate_log` (
589 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', 602 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
590 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 603 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
591 PRIMARY KEY (`id`) USING BTREE 604 PRIMARY KEY (`id`) USING BTREE
592 -) ENGINE=InnoDB AUTO_INCREMENT=65 DEFAULT CHARSET=utf8mb4 COMMENT='æ“作日志记录'; 605 +) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8mb4 COMMENT='æ“作日志记录';
593 606
594 -- ---------------------------- 607 -- ----------------------------
595 -- Records of sys_operate_log 608 -- Records of sys_operate_log
@@ -835,6 +848,118 @@ INSERT INTO `sys_role_menu` VALUES (238, 101, 1065, &#39;&#39;, &#39;2021-01-21 03:23:27&#39;, &#39; @@ -835,6 +848,118 @@ INSERT INTO `sys_role_menu` VALUES (238, 101, 1065, &#39;&#39;, &#39;2021-01-21 03:23:27&#39;, &#39;
835 COMMIT; 848 COMMIT;
836 849
837 -- ---------------------------- 850 -- ----------------------------
  851 +-- Table structure for sys_sms_channel
  852 +-- ----------------------------
  853 +DROP TABLE IF EXISTS `sys_sms_channel`;
  854 +CREATE TABLE `sys_sms_channel` (
  855 + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ç¼–å·',
  856 + `signature` varchar(10) NOT NULL COMMENT '短信签å',
  857 + `code` varchar(63) NOT NULL COMMENT '渠é“ç¼–ç ',
  858 + `status` tinyint(4) NOT NULL COMMENT 'å¼€å¯çжæ€',
  859 + `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  860 + `api_key` varchar(63) NOT NULL COMMENT '短信 API 的账å·',
  861 + `api_secret` varchar(63) DEFAULT NULL COMMENT '短信 API 的秘钥',
  862 + `callback_url` varchar(255) DEFAULT NULL COMMENT '短信å‘é€å›žè°ƒ URL',
  863 + `creator` varchar(64) DEFAULT '' COMMENT '创建者',
  864 + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  865 + `updater` varchar(64) DEFAULT '' COMMENT '更新者',
  866 + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
  867 + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
  868 + PRIMARY KEY (`id`) USING BTREE
  869 +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='短信渠é“';
  870 +
  871 +-- ----------------------------
  872 +-- Records of sys_sms_channel
  873 +-- ----------------------------
  874 +BEGIN;
  875 +INSERT INTO `sys_sms_channel` VALUES (1, '芋é“', 'YUN_PIAN', 0, '呵呵呵哒', '1555a14277cb8a608cf45a9e6a80d510', NULL, 'http://java.nat300.top/api/system/sms/callback/sms/yunpian', '', '2021-03-31 06:12:20', '1', '2021-04-05 21:02:38', b'0');
  876 +INSERT INTO `sys_sms_channel` VALUES (2, 'Ballcat', 'ALIYUN', 0, '啦啦啦', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2021-04-05 21:02:44', b'0');
  877 +INSERT INTO `sys_sms_channel` VALUES (3, '测试', 'YUN_PIAN', 0, '哈哈哈', '23132', NULL, 'http://www.baidu.com', '1', '2021-04-05 21:10:34', '1', '2021-04-05 21:10:34', b'0');
  878 +COMMIT;
  879 +
  880 +-- ----------------------------
  881 +-- Table structure for sys_sms_log
  882 +-- ----------------------------
  883 +DROP TABLE IF EXISTS `sys_sms_log`;
  884 +CREATE TABLE `sys_sms_log` (
  885 + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ç¼–å·',
  886 + `channel_id` bigint(20) NOT NULL COMMENT '短信渠é“ç¼–å·',
  887 + `channel_code` varchar(63) NOT NULL COMMENT '短信渠é“ç¼–ç ',
  888 + `template_id` bigint(20) NOT NULL COMMENT '模æ¿ç¼–å·',
  889 + `template_code` varchar(63) NOT NULL COMMENT '模æ¿ç¼–ç ',
  890 + `template_type` tinyint(4) NOT NULL COMMENT '短信类型',
  891 + `template_content` varchar(255) NOT NULL COMMENT '短信内容',
  892 + `template_params` varchar(255) NOT NULL COMMENT 'çŸ­ä¿¡å‚æ•°',
  893 + `api_template_id` varchar(63) NOT NULL COMMENT '短信 API 的模æ¿ç¼–å·',
  894 + `mobile` varchar(11) NOT NULL COMMENT '手机å·',
  895 + `user_id` bigint(20) DEFAULT '0' COMMENT '用户编å·',
  896 + `user_type` tinyint(4) DEFAULT '0' COMMENT '用户类型',
  897 + `send_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'å‘é€çжæ€',
  898 + `send_time` datetime DEFAULT NULL COMMENT 'å‘逿—¶é—´',
  899 + `send_code` int(11) DEFAULT NULL COMMENT 'å‘é€ç»“果的编ç ',
  900 + `send_msg` varchar(255) DEFAULT NULL COMMENT 'å‘é€ç»“果的æç¤º',
  901 + `api_send_code` varchar(63) DEFAULT NULL COMMENT '短信 API å‘é€ç»“果的编ç ',
  902 + `api_send_msg` varchar(255) DEFAULT NULL COMMENT '短信 API å‘é€å¤±è´¥çš„æç¤º',
  903 + `api_request_id` varchar(255) DEFAULT NULL COMMENT '短信 API å‘é€è¿”回的唯一请求 ID',
  904 + `api_serial_no` varchar(255) DEFAULT NULL COMMENT '短信 API å‘é€è¿”回的åºå·',
  905 + `receive_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '接收状æ€',
  906 + `receive_time` datetime DEFAULT NULL COMMENT '接收时间',
  907 + `api_receive_code` varchar(63) DEFAULT NULL COMMENT 'API 接收结果的编ç ',
  908 + `api_receive_msg` varchar(255) DEFAULT NULL COMMENT 'API 接收结果的说明',
  909 + `creator` varchar(64) DEFAULT '' COMMENT '创建者',
  910 + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  911 + `updater` varchar(64) DEFAULT '' COMMENT '更新者',
  912 + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
  913 + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
  914 + PRIMARY KEY (`id`) USING BTREE
  915 +) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COMMENT='短信moan';
  916 +
  917 +-- ----------------------------
  918 +-- Records of sys_sms_log
  919 +-- ----------------------------
  920 +BEGIN;
  921 +INSERT INTO `sys_sms_log` VALUES (15, 1, 'YUN_PIAN', 2, 'test_01', 1, '正在进行登录æ“作登陆,您的验è¯ç æ˜¯1234', '{\"code\":\"1234\",\"operation\":\"登陆\"}', '4383920', '15601691399', 1, 2, 10, '2021-04-04 23:24:13', 0, 'æˆåŠŸ', '0', 'å‘逿ˆåŠŸ', NULL, '62922707786', 10, '2021-04-04 23:24:26', 'DELIVRD', 'DELIVRD', NULL, '2021-04-04 23:23:29', NULL, '2021-04-04 23:25:17', b'0');
  922 +INSERT INTO `sys_sms_log` VALUES (16, 2, 'ALIYUN', 3, 'test_02', 1, '您的验è¯ç 1234,该验è¯ç 5分钟内有效,请勿泄æ¼äºŽä»–人ï¼', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:08:39', 999, '未知错误,需è¦è§£æž', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : 2EA9C6A5-579F-4D21-B7D3-0AD3BA4F7741', '2EA9C6A5-579F-4D21-B7D3-0AD3BA4F7741', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:08:39', NULL, '2021-04-05 00:08:39', b'0');
  923 +INSERT INTO `sys_sms_log` VALUES (17, 2, 'ALIYUN', 3, 'test_02', 1, '您的验è¯ç 1234,该验è¯ç 5分钟内有效,请勿泄æ¼äºŽä»–人ï¼', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:09:43', 999, '未知错误,需è¦è§£æž', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : BF766164-9C03-44FD-B6D3-ADA74118E432', 'BF766164-9C03-44FD-B6D3-ADA74118E432', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:09:43', NULL, '2021-04-05 00:09:43', b'0');
  924 +INSERT INTO `sys_sms_log` VALUES (18, 2, 'ALIYUN', 3, 'test_02', 1, '您的验è¯ç 1234,该验è¯ç 5分钟内有效,请勿泄æ¼äºŽä»–人ï¼', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:11:13', 999, '未知错误,需è¦è§£æž', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : 2D7C0ABC-7538-45B4-BFEF-B610D591CE3D', '2D7C0ABC-7538-45B4-BFEF-B610D591CE3D', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:11:12', NULL, '2021-04-05 00:11:13', b'0');
  925 +INSERT INTO `sys_sms_log` VALUES (19, 2, 'ALIYUN', 3, 'test_02', 1, '您的验è¯ç 1234,该验è¯ç 5分钟内有效,请勿泄æ¼äºŽä»–人ï¼', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:12:21', 999, '未知错误,需è¦è§£æž', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : 0A86DC5C-2985-474F-B076-748C9F2C5D3F', '0A86DC5C-2985-474F-B076-748C9F2C5D3F', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:12:01', NULL, '2021-04-05 00:12:21', b'0');
  926 +INSERT INTO `sys_sms_log` VALUES (20, 1, 'YUN_PIAN', 2, 'test_01', 1, '正在进行登录æ“作登陆,您的验è¯ç æ˜¯1234', '{\"code\":\"1234\",\"operation\":\"登陆\"}', '4383920', '15601691399', 1, 2, 10, '2021-04-05 00:14:36', 0, 'æˆåŠŸ', '0', 'å‘逿ˆåŠŸ', NULL, '62923244790', 0, NULL, NULL, NULL, NULL, '2021-04-05 00:13:42', NULL, '2021-04-05 00:14:36', b'0');
  927 +INSERT INTO `sys_sms_log` VALUES (21, 2, 'ALIYUN', 3, 'test_02', 1, '您的验è¯ç 1234,该验è¯ç 5分钟内有效,请勿泄æ¼äºŽä»–人ï¼', '{\"code\":\"1234\"}', 'SMS_207945135', '15601691399', 1, 2, 20, '2021-04-05 00:19:43', 999, '未知错误,需è¦è§£æž', 'SDK.InvalidAccessKeySecret', 'SDK.InvalidAccessKeySecret : Specified Access Key Secret is not valid.\r\nRequestId : 3837C6D3-B96F-428C-BBB2-86135D4B5B99', '3837C6D3-B96F-428C-BBB2-86135D4B5B99', NULL, 0, NULL, NULL, NULL, NULL, '2021-04-05 00:15:06', NULL, '2021-04-05 00:19:44', b'0');
  928 +COMMIT;
  929 +
  930 +-- ----------------------------
  931 +-- Table structure for sys_sms_template
  932 +-- ----------------------------
  933 +DROP TABLE IF EXISTS `sys_sms_template`;
  934 +CREATE TABLE `sys_sms_template` (
  935 + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ç¼–å·',
  936 + `type` tinyint(4) NOT NULL COMMENT '短信签å',
  937 + `status` tinyint(4) NOT NULL COMMENT 'å¼€å¯çжæ€',
  938 + `code` varchar(63) NOT NULL COMMENT '模æ¿ç¼–ç ',
  939 + `name` varchar(63) NOT NULL COMMENT '模æ¿åç§°',
  940 + `content` varchar(255) NOT NULL COMMENT '模æ¿å†…容',
  941 + `params` varchar(255) NOT NULL COMMENT '傿•°æ•°ç»„',
  942 + `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  943 + `api_template_id` varchar(63) NOT NULL COMMENT '短信 API 的模æ¿ç¼–å·',
  944 + `channel_id` bigint(20) NOT NULL COMMENT '短信渠é“ç¼–å·',
  945 + `channel_code` varchar(63) NOT NULL COMMENT '短信渠é“ç¼–ç ',
  946 + `creator` varchar(64) DEFAULT '' COMMENT '创建者',
  947 + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  948 + `updater` varchar(64) DEFAULT '' COMMENT '更新者',
  949 + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
  950 + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
  951 + PRIMARY KEY (`id`) USING BTREE
  952 +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='短信模æ¿';
  953 +
  954 +-- ----------------------------
  955 +-- Records of sys_sms_template
  956 +-- ----------------------------
  957 +BEGIN;
  958 +INSERT INTO `sys_sms_template` VALUES (2, 1, 0, 'test_01', '测试验è¯ç çŸ­ä¿¡', '正在进行登录æ“作{operation},您的验è¯ç æ˜¯{code}', '[\"operation\",\"code\"]', NULL, '4383920', 1, 'YUN_PIAN', '', '2021-03-31 10:49:38', '', '2021-03-31 12:01:38', b'0');
  959 +INSERT INTO `sys_sms_template` VALUES (3, 1, 0, 'test_02', '公告通知', '您的验è¯ç {code},该验è¯ç 5分钟内有效,请勿泄æ¼äºŽä»–人ï¼', '[\"code\"]', NULL, 'SMS_207945135', 2, 'ALIYUN', '', '2021-03-31 11:56:30', '', '2021-03-31 11:56:30', b'0');
  960 +COMMIT;
  961 +
  962 +-- ----------------------------
838 -- Table structure for sys_user 963 -- Table structure for sys_user
839 -- ---------------------------- 964 -- ----------------------------
840 DROP TABLE IF EXISTS `sys_user`; 965 DROP TABLE IF EXISTS `sys_user`;
@@ -865,8 +990,8 @@ CREATE TABLE `sys_user` ( @@ -865,8 +990,8 @@ CREATE TABLE `sys_user` (
865 -- Records of sys_user 990 -- Records of sys_user
866 -- ---------------------------- 991 -- ----------------------------
867 BEGIN; 992 BEGIN;
868 -INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋é“', '管ç†å‘˜', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:8080/api/system/file/get/add5ec1891a7d97d2cc1d60847e16294.jpg', 0, '127.0.0.1', '2021-01-05 17:03:47', 'admin', '2021-01-05 17:03:47', '1', '2021-03-21 18:16:16', b'0');  
869 -INSERT INTO `sys_user` VALUES (2, 'ry', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', 'è‹¥ä¾', '测试员', 105, '[2]', 'ry@qq.com', '15666666666', 1, '', 0, '127.0.0.1', '2021-01-05 17:03:47', 'admin', '2021-01-05 17:03:47', '', '2021-01-05 17:03:47', b'0'); 993 +INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', 'èŠ‹é“æºç ', '管ç†å‘˜', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://api-dashboard.yudao.iocoder.cn/api/infra/file/get/5e8609290e915c4fa8b08e67.jpg', 0, '127.0.0.1', '2021-01-05 17:03:47', 'admin', '2021-01-05 17:03:47', '1', '2021-04-05 02:16:10', b'0');
  994 +INSERT INTO `sys_user` VALUES (2, 'ry', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', 'è‹¥ä¾', '测试员', 105, '[2]', 'ry@qq.com', '15666666666', 1, '', 0, '127.0.0.1', '2021-01-05 17:03:47', 'admin', '2021-01-05 17:03:47', '', '2021-04-01 04:50:36', b'1');
870 INSERT INTO `sys_user` VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋é“', 'ä¸è¦å“我', 100, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '', NULL, '', '2021-01-07 09:07:17', '1', '2021-03-14 22:35:17', b'0'); 995 INSERT INTO `sys_user` VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋é“', 'ä¸è¦å“我', 100, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '', NULL, '', '2021-01-07 09:07:17', '1', '2021-03-14 22:35:17', b'0');
871 INSERT INTO `sys_user` VALUES (103, 'yuanma', '', 'æºç ', NULL, 100, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '', NULL, '', '2021-01-13 23:50:35', '', '2021-01-13 23:50:35', b'0'); 996 INSERT INTO `sys_user` VALUES (103, 'yuanma', '', 'æºç ', NULL, 100, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '', NULL, '', '2021-01-13 23:50:35', '', '2021-01-13 23:50:35', b'0');
872 INSERT INTO `sys_user` VALUES (104, 'test', '$2a$10$.TOFpaIiI3PzEwkGrNq0Eu6Cc3rOqJMxTb1DqeSEM8StxaGPBRKoi', '测试å·', NULL, 100, '[]', '', '15601691200', 1, '', 0, '', NULL, '', '2021-01-21 02:13:53', '1', '2021-03-14 22:36:38', b'0'); 997 INSERT INTO `sys_user` VALUES (104, 'test', '$2a$10$.TOFpaIiI3PzEwkGrNq0Eu6Cc3rOqJMxTb1DqeSEM8StxaGPBRKoi', '测试å·', NULL, 100, '[]', '', '15601691200', 1, '', 0, '', NULL, '', '2021-01-21 02:13:53', '1', '2021-03-14 22:36:38', b'0');
@@ -924,11 +1049,17 @@ CREATE TABLE `sys_user_session` ( @@ -924,11 +1049,17 @@ CREATE TABLE `sys_user_session` (
924 -- Records of sys_user_session 1049 -- Records of sys_user_session
925 -- ---------------------------- 1050 -- ----------------------------
926 BEGIN; 1051 BEGIN;
  1052 +INSERT INTO `sys_user_session` VALUES ('04c6624c7bf14b1ba1a01cb976a9d876', 1, '2021-04-05 21:40:12', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 20:21:09', NULL, '2021-04-01 12:25:35', b'1');
  1053 +INSERT INTO `sys_user_session` VALUES ('0e235ce5ae7342a09b372a00bd7d1b41', 1, '2021-04-05 01:43:22', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 00:51:03', NULL, '2021-04-01 04:18:06', b'1');
  1054 +INSERT INTO `sys_user_session` VALUES ('0e6943f8ca9b4215a014843eb489ccc7', 1, '2021-04-05 22:53:22', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 21:43:59', NULL, '2021-04-05 22:23:22', b'0');
  1055 +INSERT INTO `sys_user_session` VALUES ('40d532d8900c43b791266429a7911751', 1, '2021-04-05 22:11:34', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 21:41:34', NULL, '2021-04-01 12:28:20', b'1');
927 INSERT INTO `sys_user_session` VALUES ('505b4e7d8b0d4b40aa23bf540da81234', 1, '2021-03-14 01:25:13', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-14 00:31:43', NULL, '2021-03-13 07:35:26', b'1'); 1056 INSERT INTO `sys_user_session` VALUES ('505b4e7d8b0d4b40aa23bf540da81234', 1, '2021-03-14 01:25:13', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-14 00:31:43', NULL, '2021-03-13 07:35:26', b'1');
928 INSERT INTO `sys_user_session` VALUES ('5a7248bf87d14e7e9f0578b05969986c', 1, '2021-03-13 10:42:50', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-13 09:37:36', NULL, '2021-03-12 19:53:07', b'1'); 1057 INSERT INTO `sys_user_session` VALUES ('5a7248bf87d14e7e9f0578b05969986c', 1, '2021-03-13 10:42:50', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-13 09:37:36', NULL, '2021-03-12 19:53:07', b'1');
  1058 +INSERT INTO `sys_user_session` VALUES ('8b3eac5e4a104a4191c8070e03d553ea', 1, '2021-04-05 02:45:12', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-04-05 02:15:12', NULL, '2021-04-01 11:05:25', b'1');
929 INSERT INTO `sys_user_session` VALUES ('9ae27346d8b7491aad1385f51e8aa196', 1, '2021-03-13 14:02:12', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-13 10:43:06', NULL, '2021-03-13 06:40:35', b'1'); 1059 INSERT INTO `sys_user_session` VALUES ('9ae27346d8b7491aad1385f51e8aa196', 1, '2021-03-13 14:02:12', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-13 10:43:06', NULL, '2021-03-13 06:40:35', b'1');
930 INSERT INTO `sys_user_session` VALUES ('ae9ee7452ee54e4b983d658188c15c4d', 1, '2021-03-14 21:32:57', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-14 20:25:00', NULL, '2021-03-13 15:19:10', b'1'); 1060 INSERT INTO `sys_user_session` VALUES ('ae9ee7452ee54e4b983d658188c15c4d', 1, '2021-03-14 21:32:57', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-14 20:25:00', NULL, '2021-03-13 15:19:10', b'1');
931 -INSERT INTO `sys_user_session` VALUES ('d0adf48f82914212b947e5ab04d9fb65', 1, '2021-03-21 19:16:28', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-03-21 18:13:37', NULL, '2021-03-21 18:46:28', b'0'); 1061 +INSERT INTO `sys_user_session` VALUES ('d0adf48f82914212b947e5ab04d9fb65', 1, '2021-03-21 19:16:28', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-03-21 18:13:37', NULL, '2021-03-15 05:53:20', b'1');
  1062 +INSERT INTO `sys_user_session` VALUES ('e80c2400724042a2ab73732166cde8fc', 1, '2021-03-21 21:17:12', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-03-21 20:47:12', NULL, '2021-03-15 08:18:56', b'1');
932 INSERT INTO `sys_user_session` VALUES ('e8872f5192584440a548641b83c877ef', 1, '2021-03-21 18:36:01', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-03-21 17:51:48', NULL, '2021-03-15 03:54:20', b'1'); 1063 INSERT INTO `sys_user_session` VALUES ('e8872f5192584440a548641b83c877ef', 1, '2021-03-21 18:36:01', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', NULL, '2021-03-21 17:51:48', NULL, '2021-03-15 03:54:20', b'1');
933 INSERT INTO `sys_user_session` VALUES ('f1ab99b09b5a475795579ff99d60ac78', 1, '2021-03-14 23:04:31', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-14 21:12:44', NULL, '2021-03-15 03:32:38', b'1'); 1064 INSERT INTO `sys_user_session` VALUES ('f1ab99b09b5a475795579ff99d60ac78', 1, '2021-03-14 23:04:31', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-14 21:12:44', NULL, '2021-03-15 03:32:38', b'1');
934 INSERT INTO `sys_user_session` VALUES ('f853b50d064340a581e9a49bba9411fc', 1, '2021-03-10 01:55:41', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-10 01:11:53', NULL, '2021-03-12 18:37:05', b'1'); 1065 INSERT INTO `sys_user_session` VALUES ('f853b50d064340a581e9a49bba9411fc', 1, '2021-03-10 01:55:41', 'admin', '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36', NULL, '2021-03-10 01:11:53', NULL, '2021-03-12 18:37:05', b'1');
@@ -964,7 +1095,7 @@ CREATE TABLE `tool_codegen_column` ( @@ -964,7 +1095,7 @@ CREATE TABLE `tool_codegen_column` (
964 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', 1095 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
965 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 1096 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
966 PRIMARY KEY (`id`) USING BTREE 1097 PRIMARY KEY (`id`) USING BTREE
967 -) ENGINE=InnoDB AUTO_INCREMENT=389 DEFAULT CHARSET=utf8mb4 COMMENT='代ç ç”Ÿæˆè¡¨å­—段定义'; 1098 +) ENGINE=InnoDB AUTO_INCREMENT=418 DEFAULT CHARSET=utf8mb4 COMMENT='代ç ç”Ÿæˆè¡¨å­—段定义';
968 1099
969 -- ---------------------------- 1100 -- ----------------------------
970 -- Records of tool_codegen_column 1101 -- Records of tool_codegen_column
@@ -1126,6 +1257,35 @@ INSERT INTO `tool_codegen_column` VALUES (385, 33, &#39;create_time&#39;, &#39;datetime&#39;, &#39;å @@ -1126,6 +1257,35 @@ INSERT INTO `tool_codegen_column` VALUES (385, 33, &#39;create_time&#39;, &#39;datetime&#39;, &#39;å
1126 INSERT INTO `tool_codegen_column` VALUES (386, 33, 'updater', 'varchar(64)', '更新者', b'1', b'0', '0', 6, 'String', 'updater', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0'); 1257 INSERT INTO `tool_codegen_column` VALUES (386, 33, 'updater', 'varchar(64)', '更新者', b'1', b'0', '0', 6, 'String', 'updater', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0');
1127 INSERT INTO `tool_codegen_column` VALUES (387, 33, 'update_time', 'datetime', 'æ›´æ–°æ—¶é—´', b'0', b'0', '0', 7, 'Date', 'updateTime', '', NULL, b'0', b'0', b'0', 'BETWEEN', b'0', 'datetime', '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0'); 1258 INSERT INTO `tool_codegen_column` VALUES (387, 33, 'update_time', 'datetime', 'æ›´æ–°æ—¶é—´', b'0', b'0', '0', 7, 'Date', 'updateTime', '', NULL, b'0', b'0', b'0', 'BETWEEN', b'0', 'datetime', '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0');
1128 INSERT INTO `tool_codegen_column` VALUES (388, 33, 'deleted', 'bit(1)', '是å¦åˆ é™¤', b'0', b'0', '0', 8, 'Boolean', 'deleted', '', NULL, b'0', b'0', b'0', '=', b'0', 'radio', '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0'); 1259 INSERT INTO `tool_codegen_column` VALUES (388, 33, 'deleted', 'bit(1)', '是å¦åˆ é™¤', b'0', b'0', '0', 8, 'Boolean', 'deleted', '', NULL, b'0', b'0', b'0', '=', b'0', 'radio', '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0');
  1260 +INSERT INTO `tool_codegen_column` VALUES (389, 34, 'id', 'bigint(20)', 'ç¼–å·', b'0', b'1', '1', 1, 'Long', 'id', '', '1024', b'0', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1261 +INSERT INTO `tool_codegen_column` VALUES (390, 34, 'signature', 'varchar(8)', '短信签å', b'0', b'0', '0', 2, 'String', 'signature', '', 'èŠ‹é“æºç ', b'1', b'1', b'1', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1262 +INSERT INTO `tool_codegen_column` VALUES (391, 34, 'code', 'varchar(63)', '渠é“ç¼–ç ', b'0', b'0', '0', 3, 'String', 'code', 'sys_sms_channel_code', 'YUN_PIAN', b'1', b'0', b'0', '=', b'1', 'select', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1263 +INSERT INTO `tool_codegen_column` VALUES (392, 34, 'status', 'tinyint(4)', 'å¯ç”¨çжæ€', b'0', b'0', '0', 4, 'Integer', 'status', 'sys_common_status', '1', b'1', b'1', b'1', '=', b'1', 'radio', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1264 +INSERT INTO `tool_codegen_column` VALUES (393, 34, 'remark', 'varchar(255)', '备注', b'1', b'0', '0', 5, 'String', 'remark', '', '好åƒï¼', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1265 +INSERT INTO `tool_codegen_column` VALUES (394, 34, 'api_key', 'varchar(63)', '短信 API 的账å·', b'0', b'0', '0', 6, 'String', 'apiKey', '', 'yudao', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1266 +INSERT INTO `tool_codegen_column` VALUES (395, 34, 'api_secret', 'varchar(63)', '短信 API 的秘钥', b'1', b'0', '0', 7, 'String', 'apiSecret', '', 'yuanma', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1267 +INSERT INTO `tool_codegen_column` VALUES (396, 34, 'callback_url', 'varchar(255)', '短信å‘é€å›žè°ƒ URL', b'1', b'0', '0', 8, 'String', 'callbackUrl', '', 'http://www.iocoder.cn', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1268 +INSERT INTO `tool_codegen_column` VALUES (397, 34, 'creator', 'varchar(64)', '创建者', b'1', b'0', '0', 9, 'String', 'creator', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1269 +INSERT INTO `tool_codegen_column` VALUES (398, 34, 'create_time', 'datetime', '创建时间', b'0', b'0', '0', 10, 'Date', 'createTime', '', NULL, b'0', b'0', b'1', 'BETWEEN', b'1', 'datetime', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1270 +INSERT INTO `tool_codegen_column` VALUES (399, 34, 'updater', 'varchar(64)', '更新者', b'1', b'0', '0', 11, 'String', 'updater', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1271 +INSERT INTO `tool_codegen_column` VALUES (400, 34, 'update_time', 'datetime', 'æ›´æ–°æ—¶é—´', b'0', b'0', '0', 12, 'Date', 'updateTime', '', NULL, b'0', b'0', b'0', 'BETWEEN', b'0', 'datetime', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1272 +INSERT INTO `tool_codegen_column` VALUES (401, 34, 'deleted', 'bit(1)', '是å¦åˆ é™¤', b'0', b'0', '0', 13, 'Boolean', 'deleted', '', NULL, b'0', b'0', b'0', '=', b'0', 'radio', '1', '2021-04-05 00:51:34', '1', '2021-04-05 20:52:09', b'0');
  1273 +INSERT INTO `tool_codegen_column` VALUES (402, 35, 'id', 'bigint(20)', 'ç¼–å·', b'0', b'1', '1', 1, 'Long', 'id', '', '1024', b'0', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1274 +INSERT INTO `tool_codegen_column` VALUES (403, 35, 'type', 'tinyint(4)', '短信签å', b'0', b'0', '0', 2, 'Integer', 'type', 'sys_sms_template_type', '1', b'1', b'1', b'1', '=', b'1', 'select', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1275 +INSERT INTO `tool_codegen_column` VALUES (404, 35, 'status', 'tinyint(4)', 'å¼€å¯çжæ€', b'0', b'0', '0', 3, 'Integer', 'status', 'sys_common_status', '1', b'1', b'1', b'1', '=', b'1', 'radio', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1276 +INSERT INTO `tool_codegen_column` VALUES (405, 35, 'code', 'varchar(63)', '模æ¿ç¼–ç ', b'0', b'0', '0', 4, 'String', 'code', '', 'test_01', b'1', b'1', b'1', 'LIKE', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1277 +INSERT INTO `tool_codegen_column` VALUES (406, 35, 'name', 'varchar(63)', '模æ¿åç§°', b'0', b'0', '0', 5, 'String', 'name', '', 'yudao', b'1', b'1', b'0', 'LIKE', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1278 +INSERT INTO `tool_codegen_column` VALUES (407, 35, 'content', 'varchar(255)', '模æ¿å†…容', b'0', b'0', '0', 6, 'String', 'content', '', '你好,{name}。你长的太{like}啦ï¼', b'1', b'1', b'1', 'LIKE', b'1', 'editor', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1279 +INSERT INTO `tool_codegen_column` VALUES (408, 35, 'params', 'varchar(255)', '傿•°æ•°ç»„', b'0', b'0', '0', 7, 'String', 'params', '', 'name,code', b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1280 +INSERT INTO `tool_codegen_column` VALUES (409, 35, 'remark', 'varchar(255)', '备注', b'1', b'0', '0', 8, 'String', 'remark', '', '哈哈哈', b'1', b'1', b'0', '=', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1281 +INSERT INTO `tool_codegen_column` VALUES (410, 35, 'api_template_id', 'varchar(63)', '短信 API 的模æ¿ç¼–å·', b'0', b'0', '0', 9, 'String', 'apiTemplateId', '', '4383920', b'1', b'1', b'1', 'LIKE', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1282 +INSERT INTO `tool_codegen_column` VALUES (411, 35, 'channel_id', 'bigint(20)', '短信渠é“ç¼–å·', b'0', b'0', '0', 10, 'Long', 'channelId', '', '10', b'1', b'1', b'1', '=', b'1', 'select', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1283 +INSERT INTO `tool_codegen_column` VALUES (412, 35, 'channel_code', 'varchar(63)', '短信渠é“ç¼–ç ', b'0', b'0', '0', 11, 'String', 'channelCode', 'sys_sms_channel_code', 'ALIYUN', b'0', b'0', b'0', '=', b'1', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1284 +INSERT INTO `tool_codegen_column` VALUES (413, 35, 'creator', 'varchar(64)', '创建者', b'1', b'0', '0', 12, 'String', 'creator', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1285 +INSERT INTO `tool_codegen_column` VALUES (414, 35, 'create_time', 'datetime', '创建时间', b'0', b'0', '0', 13, 'Date', 'createTime', '', NULL, b'0', b'0', b'1', 'BETWEEN', b'1', 'datetime', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1286 +INSERT INTO `tool_codegen_column` VALUES (415, 35, 'updater', 'varchar(64)', '更新者', b'1', b'0', '0', 14, 'String', 'updater', '', NULL, b'0', b'0', b'0', '=', b'0', 'input', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1287 +INSERT INTO `tool_codegen_column` VALUES (416, 35, 'update_time', 'datetime', 'æ›´æ–°æ—¶é—´', b'0', b'0', '0', 15, 'Date', 'updateTime', '', NULL, b'0', b'0', b'0', 'BETWEEN', b'0', 'datetime', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
  1288 +INSERT INTO `tool_codegen_column` VALUES (417, 35, 'deleted', 'bit(1)', '是å¦åˆ é™¤', b'0', b'0', '0', 16, 'Boolean', 'deleted', '', NULL, b'0', b'0', b'0', '=', b'0', 'radio', '1', '2021-04-05 21:42:22', '1', '2021-04-05 22:23:38', b'0');
1129 COMMIT; 1289 COMMIT;
1130 1290
1131 -- ---------------------------- 1291 -- ----------------------------
@@ -1151,7 +1311,7 @@ CREATE TABLE `tool_codegen_table` ( @@ -1151,7 +1311,7 @@ CREATE TABLE `tool_codegen_table` (
1151 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´', 1311 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ›´æ–°æ—¶é—´',
1152 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤', 1312 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是å¦åˆ é™¤',
1153 PRIMARY KEY (`id`) USING BTREE 1313 PRIMARY KEY (`id`) USING BTREE
1154 -) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COMMENT='代ç ç”Ÿæˆè¡¨å®šä¹‰'; 1314 +) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4 COMMENT='代ç ç”Ÿæˆè¡¨å®šä¹‰';
1155 1315
1156 -- ---------------------------- 1316 -- ----------------------------
1157 -- Records of tool_codegen_table 1317 -- Records of tool_codegen_table
@@ -1169,6 +1329,8 @@ INSERT INTO `tool_codegen_table` VALUES (28, 1, &#39;sys_dict_type&#39;, &#39;å­—å…¸ç±»åž‹è¡ @@ -1169,6 +1329,8 @@ INSERT INTO `tool_codegen_table` VALUES (28, 1, &#39;sys_dict_type&#39;, &#39;字典类型è¡
1169 INSERT INTO `tool_codegen_table` VALUES (29, 1, 'sys_dict_type', '字典类型表', NULL, 'system', 'dict', 'SysDictType', '字典类型', '芋艿', 1, NULL, '', '2021-03-06 03:52:57', '', '2021-03-06 04:03:52', b'0'); 1329 INSERT INTO `tool_codegen_table` VALUES (29, 1, 'sys_dict_type', '字典类型表', NULL, 'system', 'dict', 'SysDictType', '字典类型', '芋艿', 1, NULL, '', '2021-03-06 03:52:57', '', '2021-03-06 04:03:52', b'0');
1170 INSERT INTO `tool_codegen_table` VALUES (30, 1, 'sys_dict_data', '字典数æ®è¡¨', NULL, 'system', 'type', 'SysDictData', '字典数æ®', 'èŠ‹é“æºç ', 1, NULL, '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:47', b'0'); 1330 INSERT INTO `tool_codegen_table` VALUES (30, 1, 'sys_dict_data', '字典数æ®è¡¨', NULL, 'system', 'type', 'SysDictData', '字典数æ®', 'èŠ‹é“æºç ', 1, NULL, '', '2021-03-06 06:48:28', '', '2021-03-06 06:50:47', b'0');
1171 INSERT INTO `tool_codegen_table` VALUES (33, 1, 'inf_file', '文件表', NULL, 'infra', 'file', 'InfFile', '文件', '芋艿', 1, 2, '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0'); 1331 INSERT INTO `tool_codegen_table` VALUES (33, 1, 'inf_file', '文件表', NULL, 'infra', 'file', 'InfFile', '文件', '芋艿', 1, 2, '1', '2021-03-13 09:43:20', '1', '2021-03-13 11:27:12', b'0');
  1332 +INSERT INTO `tool_codegen_table` VALUES (34, 1, 'sys_sms_channel', '短信渠é“', NULL, 'system', 'sms', 'SysSmsChannel', '短信渠é“', 'èŠ‹é“æºç ', 1, 1093, '1', '2021-04-03 13:39:06', '1', '2021-04-05 20:52:09', b'0');
  1333 +INSERT INTO `tool_codegen_table` VALUES (35, 1, 'sys_sms_template', '短信模æ¿', NULL, 'system', 'sms', 'SysSmsTemplate', '短信模æ¿', 'èŠ‹é“æºç ', 1, 1093, '1', '2021-04-03 13:58:55', '1', '2021-04-05 22:23:38', b'0');
1172 COMMIT; 1334 COMMIT;
1173 1335
1174 -- ---------------------------- 1336 -- ----------------------------
src/main/java/cn/iocoder/dashboard/common/core/KeyValue.java 0 → 100644
  1 +package cn.iocoder.dashboard.common.core;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Data;
  5 +import lombok.NoArgsConstructor;
  6 +
  7 +/**
  8 + * Key Value 的键值对
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +@NoArgsConstructor
  14 +@AllArgsConstructor
  15 +public class KeyValue<K, V> {
  16 +
  17 + private K key;
  18 + private V value;
  19 +
  20 +}
src/main/java/cn/iocoder/dashboard/common/enums/DefaultBitFieldEnum.java 0 → 100644
  1 +package cn.iocoder.dashboard.common.enums;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +
  6 +/**
  7 + * 通用状态枚举
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@Getter
  12 +@AllArgsConstructor
  13 +public enum DefaultBitFieldEnum {
  14 +
  15 + NO(0, "否"),
  16 + YES(1, "是");
  17 +
  18 + /**
  19 + * 状态值
  20 + */
  21 + private final Integer val;
  22 + /**
  23 + * 状态名
  24 + */
  25 + private final String name;
  26 +
  27 +}
src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java
1 package cn.iocoder.dashboard.common.exception; 1 package cn.iocoder.dashboard.common.exception;
2 2
  3 +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
3 import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange; 4 import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange;
4 import lombok.Data; 5 import lombok.Data;
5 6
6 /** 7 /**
7 * 错误码对象 8 * 错误码对象
8 * 9 *
9 - * 全局错误码,占用 [0, 999],参见 {@link GlobalException} 10 + * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
10 * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} 11 * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
11 * 12 *
12 * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 13 * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
@@ -21,11 +22,11 @@ public class ErrorCode { @@ -21,11 +22,11 @@ public class ErrorCode {
21 /** 22 /**
22 * 错误提示 23 * 错误提示
23 */ 24 */
24 - private final String message; 25 + private final String msg;
25 26
26 public ErrorCode(Integer code, String message) { 27 public ErrorCode(Integer code, String message) {
27 this.code = code; 28 this.code = code;
28 - this.message = message; 29 + this.msg = message;
29 } 30 }
30 31
31 } 32 }
src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java deleted
1 -package cn.iocoder.dashboard.common.exception;  
2 -  
3 -import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;  
4 -import lombok.Data;  
5 -import lombok.EqualsAndHashCode;  
6 -  
7 -/**  
8 - * 全局异常 Exception  
9 - */  
10 -@Data  
11 -@EqualsAndHashCode(callSuper = true)  
12 -public class GlobalException extends RuntimeException {  
13 -  
14 - /**  
15 - * 全局错误码  
16 - *  
17 - * @see GlobalErrorCodeConstants  
18 - */  
19 - private Integer code;  
20 - /**  
21 - * 错误提示  
22 - */  
23 - private String message;  
24 -  
25 - /**  
26 - * 空构造方法,避免反序列化问题  
27 - */  
28 - public GlobalException() {  
29 - }  
30 -  
31 - public GlobalException(ErrorCode errorCode) {  
32 - this.code = errorCode.getCode();  
33 - this.message = errorCode.getMessage();  
34 - }  
35 -  
36 - public GlobalException(Integer code, String message) {  
37 - this.code = code;  
38 - this.message = message;  
39 - }  
40 -  
41 -}  
src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java
@@ -30,7 +30,7 @@ public final class ServiceException extends RuntimeException { @@ -30,7 +30,7 @@ public final class ServiceException extends RuntimeException {
30 30
31 public ServiceException(ErrorCode errorCode) { 31 public ServiceException(ErrorCode errorCode) {
32 this.code = errorCode.getCode(); 32 this.code = errorCode.getCode();
33 - this.message = errorCode.getMessage(); 33 + this.message = errorCode.getMsg();
34 } 34 }
35 35
36 public ServiceException(Integer code, String message) { 36 public ServiceException(Integer code, String message) {
src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java
@@ -47,12 +47,12 @@ public class ServiceExceptionUtil { @@ -47,12 +47,12 @@ public class ServiceExceptionUtil {
47 // ========== 和 ServiceException 的集成 ========== 47 // ========== 和 ServiceException 的集成 ==========
48 48
49 public static ServiceException exception(ErrorCode errorCode) { 49 public static ServiceException exception(ErrorCode errorCode) {
50 - String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage()); 50 + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
51 return exception0(errorCode.getCode(), messagePattern); 51 return exception0(errorCode.getCode(), messagePattern);
52 } 52 }
53 53
54 public static ServiceException exception(ErrorCode errorCode, Object... params) { 54 public static ServiceException exception(ErrorCode errorCode, Object... params) {
55 - String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage()); 55 + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
56 return exception0(errorCode.getCode(), messagePattern, params); 56 return exception0(errorCode.getCode(), messagePattern, params);
57 } 57 }
58 58
src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java
1 package cn.iocoder.dashboard.common.pojo; 1 package cn.iocoder.dashboard.common.pojo;
2 2
3 import cn.iocoder.dashboard.common.exception.ErrorCode; 3 import cn.iocoder.dashboard.common.exception.ErrorCode;
4 -import cn.iocoder.dashboard.common.exception.GlobalException;  
5 import cn.iocoder.dashboard.common.exception.ServiceException; 4 import cn.iocoder.dashboard.common.exception.ServiceException;
6 import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; 5 import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
7 import com.fasterxml.jackson.annotation.JsonIgnore; 6 import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -9,6 +8,7 @@ import lombok.Data; @@ -9,6 +8,7 @@ import lombok.Data;
9 import org.springframework.util.Assert; 8 import org.springframework.util.Assert;
10 9
11 import java.io.Serializable; 10 import java.io.Serializable;
  11 +import java.util.Objects;
12 12
13 /** 13 /**
14 * 通用返回 14 * 通用返回
@@ -16,7 +16,7 @@ import java.io.Serializable; @@ -16,7 +16,7 @@ import java.io.Serializable;
16 * @param <T> 数据泛型 16 * @param <T> 数据泛型
17 */ 17 */
18 @Data 18 @Data
19 -public final class CommonResult<T> implements Serializable { 19 +public class CommonResult<T> implements Serializable {
20 20
21 /** 21 /**
22 * 错误码 22 * 错误码
@@ -31,7 +31,7 @@ public final class CommonResult&lt;T&gt; implements Serializable { @@ -31,7 +31,7 @@ public final class CommonResult&lt;T&gt; implements Serializable {
31 /** 31 /**
32 * 错误提示,用户可阅读 32 * 错误提示,用户可阅读
33 * 33 *
34 - * @see ErrorCode#getMessage() () 34 + * @see ErrorCode#getMsg() ()
35 */ 35 */
36 private String msg; 36 private String msg;
37 37
@@ -57,7 +57,7 @@ public final class CommonResult&lt;T&gt; implements Serializable { @@ -57,7 +57,7 @@ public final class CommonResult&lt;T&gt; implements Serializable {
57 } 57 }
58 58
59 public static <T> CommonResult<T> error(ErrorCode errorCode) { 59 public static <T> CommonResult<T> error(ErrorCode errorCode) {
60 - return error(errorCode.getCode(), errorCode.getMessage()); 60 + return error(errorCode.getCode(), errorCode.getMsg());
61 } 61 }
62 62
63 public static <T> CommonResult<T> success(T data) { 63 public static <T> CommonResult<T> success(T data) {
@@ -68,9 +68,13 @@ public final class CommonResult&lt;T&gt; implements Serializable { @@ -68,9 +68,13 @@ public final class CommonResult&lt;T&gt; implements Serializable {
68 return result; 68 return result;
69 } 69 }
70 70
  71 + public static boolean isSuccess(Integer code) {
  72 + return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
  73 + }
  74 +
71 @JsonIgnore // 避免 jackson 序列化 75 @JsonIgnore // 避免 jackson 序列化
72 public boolean isSuccess() { 76 public boolean isSuccess() {
73 - return GlobalErrorCodeConstants.SUCCESS.getCode().equals(code); 77 + return isSuccess(code);
74 } 78 }
75 79
76 @JsonIgnore // 避免 jackson 序列化 80 @JsonIgnore // 避免 jackson 序列化
@@ -81,16 +85,12 @@ public final class CommonResult&lt;T&gt; implements Serializable { @@ -81,16 +85,12 @@ public final class CommonResult&lt;T&gt; implements Serializable {
81 // ========= 和 Exception 异常体系集成 ========= 85 // ========= 和 Exception 异常体系集成 =========
82 86
83 /** 87 /**
84 - * 判断是否有异常。如果有,则抛出 {@link GlobalException} 或 {@link ServiceException} 异常 88 + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
85 */ 89 */
86 - public void checkError() throws GlobalException, ServiceException { 90 + public void checkError() throws ServiceException {
87 if (isSuccess()) { 91 if (isSuccess()) {
88 return; 92 return;
89 } 93 }
90 - // 全局异常  
91 - if (GlobalErrorCodeConstants.isMatch(code)) {  
92 - throw new GlobalException(code, msg);  
93 - }  
94 // 业务异常 94 // 业务异常
95 throw new ServiceException(code, msg); 95 throw new ServiceException(code, msg);
96 } 96 }
@@ -99,8 +99,4 @@ public final class CommonResult&lt;T&gt; implements Serializable { @@ -99,8 +99,4 @@ public final class CommonResult&lt;T&gt; implements Serializable {
99 return error(serviceException.getCode(), serviceException.getMessage()); 99 return error(serviceException.getCode(), serviceException.getMessage());
100 } 100 }
101 101
102 - public static <T> CommonResult<T> error(GlobalException globalException) {  
103 - return error(globalException.getCode(), globalException.getMessage());  
104 - }  
105 -  
106 } 102 }
src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/DictConvert.java
@@ -13,6 +13,11 @@ import com.alibaba.excel.metadata.GlobalConfiguration; @@ -13,6 +13,11 @@ import com.alibaba.excel.metadata.GlobalConfiguration;
13 import com.alibaba.excel.metadata.property.ExcelContentProperty; 13 import com.alibaba.excel.metadata.property.ExcelContentProperty;
14 import lombok.extern.slf4j.Slf4j; 14 import lombok.extern.slf4j.Slf4j;
15 15
  16 +/**
  17 + * Excel {@link SysDictDataDO} 数据字典转换器
  18 + *
  19 + * @author 芋道源码
  20 + */
16 @Slf4j 21 @Slf4j
17 public class DictConvert implements Converter<Object> { 22 public class DictConvert implements Converter<Object> {
18 23
src/main/java/cn/iocoder/dashboard/framework/excel/core/convert/JsonConvert.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.excel.core.convert;
  2 +
  3 +import cn.iocoder.dashboard.util.json.JsonUtils;
  4 +import com.alibaba.excel.converters.Converter;
  5 +import com.alibaba.excel.enums.CellDataTypeEnum;
  6 +import com.alibaba.excel.metadata.CellData;
  7 +import com.alibaba.excel.metadata.GlobalConfiguration;
  8 +import com.alibaba.excel.metadata.property.ExcelContentProperty;
  9 +
  10 +/**
  11 + * Excel Json 转换器
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +public class JsonConvert implements Converter<Object> {
  16 +
  17 + @Override
  18 + public Class<?> supportJavaTypeKey() {
  19 + throw new UnsupportedOperationException("暂不支持,也不需要");
  20 + }
  21 +
  22 + @Override
  23 + public CellDataTypeEnum supportExcelTypeKey() {
  24 + throw new UnsupportedOperationException("暂不支持,也不需要");
  25 + }
  26 +
  27 + @Override
  28 + public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
  29 + throw new UnsupportedOperationException("暂不支持,也不需要");
  30 + }
  31 +
  32 + @Override
  33 + public CellData<String> convertToExcelData(Object value, ExcelContentProperty contentProperty,
  34 + GlobalConfiguration globalConfiguration) {
  35 + // 生成 Excel 小表格
  36 + return new CellData<>(JsonUtils.toJsonString(value));
  37 + }
  38 +
  39 +}
src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java
@@ -48,8 +48,8 @@ public class RedisConfig { @@ -48,8 +48,8 @@ public class RedisConfig {
48 * 创建 Redis Pub/Sub 广播消费的容器 48 * 创建 Redis Pub/Sub 广播消费的容器
49 */ 49 */
50 @Bean 50 @Bean
51 - public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory,  
52 - List<AbstractChannelMessageListener<?>> listeners) { 51 + public RedisMessageListenerContainer redisMessageListenerContainer(
  52 + RedisConnectionFactory factory, List<AbstractChannelMessageListener<?>> listeners) {
53 // 创建 RedisMessageListenerContainer 对象 53 // 创建 RedisMessageListenerContainer 对象
54 RedisMessageListenerContainer container = new RedisMessageListenerContainer(); 54 RedisMessageListenerContainer container = new RedisMessageListenerContainer();
55 // 设置 RedisConnection 工厂。 55 // 设置 RedisConnection 工厂。
@@ -69,8 +69,8 @@ public class RedisConfig { @@ -69,8 +69,8 @@ public class RedisConfig {
69 * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html 69 * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
70 */ 70 */
71 @Bean(initMethod = "start", destroyMethod = "stop") 71 @Bean(initMethod = "start", destroyMethod = "stop")
72 - public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(RedisTemplate<String, Object> redisTemplate,  
73 - List<AbstractStreamMessageListener<?>> listeners) { 72 + public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
  73 + RedisTemplate<String, Object> redisTemplate, List<AbstractStreamMessageListener<?>> listeners) {
74 // 第一步,创建 StreamMessageListenerContainer 容器 74 // 第一步,创建 StreamMessageListenerContainer 容器
75 // 创建 options 配置 75 // 创建 options 配置
76 StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions = 76 StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java
@@ -128,13 +128,13 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @@ -128,13 +128,13 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
128 // 设置每个请求的权限 128 // 设置每个请求的权限
129 .authorizeRequests() 129 .authorizeRequests()
130 // 登陆的接口,可匿名访问 130 // 登陆的接口,可匿名访问
131 - .antMatchers(webProperties.getApiPrefix() + "/login").anonymous() 131 + .antMatchers(api("/login")).anonymous()
132 // 通用的接口,可匿名访问 132 // 通用的接口,可匿名访问
133 - .antMatchers( webProperties.getApiPrefix() + "/system/captcha/**").anonymous() 133 + .antMatchers(api("/system/captcha/**")).anonymous()
134 // 静态资源,可匿名访问 134 // 静态资源,可匿名访问
135 .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() 135 .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
136 // 文件的获取接口,可匿名访问 136 // 文件的获取接口,可匿名访问
137 - .antMatchers(webProperties.getApiPrefix() + "/infra/file/get/**").anonymous() 137 + .antMatchers(api("/infra/file/get/**")).anonymous()
138 // Swagger 接口文档 138 // Swagger 接口文档
139 .antMatchers("/swagger-ui.html").anonymous() 139 .antMatchers("/swagger-ui.html").anonymous()
140 .antMatchers("/swagger-resources/**").anonymous() 140 .antMatchers("/swagger-resources/**").anonymous()
@@ -148,13 +148,19 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @@ -148,13 +148,19 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
148 .antMatchers("/actuator/**").anonymous() 148 .antMatchers("/actuator/**").anonymous()
149 // Druid 监控 149 // Druid 监控
150 .antMatchers("/druid/**").anonymous() 150 .antMatchers("/druid/**").anonymous()
  151 + // 短信回调 API
  152 + .antMatchers(api("/system/sms/callback/**")).anonymous()
151 // 除上面外的所有请求全部需要鉴权认证 153 // 除上面外的所有请求全部需要鉴权认证
152 .anyRequest().authenticated() 154 .anyRequest().authenticated()
153 .and() 155 .and()
154 .headers().frameOptions().disable(); 156 .headers().frameOptions().disable();
155 - httpSecurity.logout().logoutUrl(webProperties.getApiPrefix() + "/logout").logoutSuccessHandler(logoutSuccessHandler); 157 + httpSecurity.logout().logoutUrl(api("/logout")).logoutSuccessHandler(logoutSuccessHandler);
156 // 添加 JWT Filter 158 // 添加 JWT Filter
157 httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); 159 httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
158 } 160 }
159 161
  162 + private String api(String url) {
  163 + return webProperties.getApiPrefix() + url;
  164 + }
  165 +
160 } 166 }
src/main/java/cn/iocoder/dashboard/framework/sms/config/SmsConfiguration.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.config;
  2 +
  3 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
  4 +import cn.iocoder.dashboard.framework.sms.core.client.impl.SmsClientFactoryImpl;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.context.annotation.Configuration;
  7 +
  8 +/**
  9 + * 短信配置类
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Configuration
  14 +public class SmsConfiguration {
  15 +
  16 + @Bean
  17 + public SmsClientFactory smsClientFactory() {
  18 + return new SmsClientFactoryImpl();
  19 + }
  20 +
  21 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClient.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client;
  2 +
  3 +import cn.iocoder.dashboard.common.core.KeyValue;
  4 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
  5 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  6 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * 短信客户端接口
  12 + *
  13 + * @author zzf
  14 + * @date 2021/1/25 14:14
  15 + */
  16 +public interface SmsClient {
  17 +
  18 + /**
  19 + * 获得渠道编号
  20 + *
  21 + * @return 渠道编号
  22 + */
  23 + Long getId();
  24 +
  25 + /**
  26 + * 发送消息
  27 + *
  28 + * @param logId 日志编号
  29 + * @param mobile 手机号
  30 + * @param apiTemplateId 短信 API 的模板编号
  31 + * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序
  32 + * @return 短信发送结果
  33 + */
  34 + SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile, String apiTemplateId,
  35 + List<KeyValue<String, Object>> templateParams);
  36 +
  37 + /**
  38 + * 解析接收短信的接收结果
  39 + *
  40 + * @param text 结果
  41 + * @return 结果内容
  42 + * @throws Throwable 当解析 text 发生异常时,则会抛出异常
  43 + */
  44 + List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable;
  45 +
  46 + /**
  47 + * 查询指定的短信模板
  48 + *
  49 + * @param apiTemplateId 短信 API 的模板编号
  50 + * @return 短信模板
  51 + */
  52 + SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId);
  53 +
  54 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsClientFactory.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client;
  2 +
  3 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  4 +
  5 +/**
  6 + * 短信客户端工厂接口
  7 + *
  8 + * @author zzf
  9 + * @date 2021/1/28 14:01
  10 + */
  11 +public interface SmsClientFactory {
  12 +
  13 + /**
  14 + * 获得短信 Client
  15 + *
  16 + * @param channelId 渠道编号
  17 + * @return 短信 Client
  18 + */
  19 + SmsClient getSmsClient(Long channelId);
  20 +
  21 + /**
  22 + * 获得短信 Client
  23 + *
  24 + * @param channelCode 渠道编码
  25 + * @return 短信 Client
  26 + */
  27 + SmsClient getSmsClient(String channelCode);
  28 +
  29 + /**
  30 + * 创建短信 Client
  31 + *
  32 + * @param properties 配置对象
  33 + */
  34 + void createOrUpdateSmsClient(SmsChannelProperties properties);
  35 +
  36 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCodeMapping.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client;
  2 +
  3 +import cn.iocoder.dashboard.common.exception.ErrorCode;
  4 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
  5 +
  6 +import java.util.function.Function;
  7 +
  8 +/**
  9 + * 将 API 的错误码,转换为通用的错误码
  10 + *
  11 + * @see SmsCommonResult
  12 + * @see SmsFrameworkErrorCodeConstants
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +public interface SmsCodeMapping extends Function<String, ErrorCode> {
  17 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/SmsCommonResult.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client;
  2 +
  3 +import cn.hutool.core.exceptions.ExceptionUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.iocoder.dashboard.common.exception.ErrorCode;
  6 +import cn.iocoder.dashboard.common.pojo.CommonResult;
  7 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
  8 +import lombok.Data;
  9 +import lombok.EqualsAndHashCode;
  10 +import lombok.ToString;
  11 +
  12 +/**
  13 + * 短信的 CommonResult 拓展类
  14 + *
  15 + * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
  16 + *
  17 + * 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段
  18 + *
  19 + * @author 芋道源码
  20 + */
  21 +@Data
  22 +@EqualsAndHashCode(callSuper = true)
  23 +@ToString(callSuper = true)
  24 +public class SmsCommonResult<T> extends CommonResult<T> {
  25 +
  26 + /**
  27 + * API 返回错误码
  28 + *
  29 + * 由于第三方的错误码可能是字符串,所以使用 String 类型
  30 + */
  31 + private String apiCode;
  32 + /**
  33 + * API 返回提示
  34 + */
  35 + private String apiMsg;
  36 +
  37 + /**
  38 + * API 请求编号
  39 + */
  40 + private String apiRequestId;
  41 +
  42 + private SmsCommonResult() {
  43 + }
  44 +
  45 + public static <T> SmsCommonResult<T> build(String apiCode, String apiMsg, String apiRequestId,
  46 + T data, SmsCodeMapping codeMapping) {
  47 + Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
  48 + SmsCommonResult<T> result = new SmsCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId);
  49 + result.setData(data);
  50 + // 翻译错误码
  51 + if (codeMapping != null) {
  52 + ErrorCode errorCode = codeMapping.apply(apiCode);
  53 + if (errorCode == null) {
  54 + errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
  55 + }
  56 + result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
  57 + }
  58 + return result;
  59 + }
  60 +
  61 + public static <T> SmsCommonResult<T> error(Throwable ex) {
  62 + SmsCommonResult<T> result = new SmsCommonResult<>();
  63 + result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode());
  64 + result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
  65 + return result;
  66 + }
  67 +
  68 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsReceiveRespDTO.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.Date;
  6 +
  7 +/**
  8 + * 消息接收 Response DTO
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class SmsReceiveRespDTO {
  14 +
  15 + /**
  16 + * 是否接收成功
  17 + */
  18 + private Boolean success;
  19 + /**
  20 + * API 接收结果的编码
  21 + */
  22 + private String errorCode;
  23 + /**
  24 + * API 接收结果的说明
  25 + */
  26 + private String errorMsg;
  27 +
  28 + /**
  29 + * 手机号
  30 + */
  31 + private String mobile;
  32 + /**
  33 + * 用户接收时间
  34 + */
  35 + private Date receiveTime;
  36 +
  37 + /**
  38 + * 短信 API 发送返回的序号
  39 + */
  40 + private String serialNo;
  41 + /**
  42 + * 短信日志编号
  43 + *
  44 + * 对应 SysSmsLogDO 的编号
  45 + */
  46 + private Long logId;
  47 +
  48 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsSendRespDTO.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 短信发送 Response DTO
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +@Data
  11 +public class SmsSendRespDTO {
  12 +
  13 + /**
  14 + * 短信 API 发送返回的序号
  15 + */
  16 + private String serialNo;
  17 +
  18 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/dto/SmsTemplateRespDTO.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.dto;
  2 +
  3 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 短信模板 Response DTO
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@Data
  12 +public class SmsTemplateRespDTO {
  13 +
  14 + /**
  15 + * 模板编号
  16 + */
  17 + private String id;
  18 + /**
  19 + * 短信内容
  20 + */
  21 + private String content;
  22 + /**
  23 + * 审核状态
  24 + *
  25 + * 枚举 {@link SmsTemplateAuditStatusEnum}
  26 + */
  27 + private Integer auditStatus;
  28 + /**
  29 + * 审核未通过的理由
  30 + */
  31 + private String auditReason;
  32 +
  33 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/AbstractSmsClient.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl;
  2 +
  3 +import cn.iocoder.dashboard.common.core.KeyValue;
  4 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
  5 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
  6 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  7 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
  8 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  9 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  10 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * 短信客户端抽象类
  17 + *
  18 + * @author zzf
  19 + * @date 2021/2/1 9:28
  20 + */
  21 +@Slf4j
  22 +public abstract class AbstractSmsClient implements SmsClient {
  23 +
  24 + /**
  25 + * 短信渠道配置
  26 + */
  27 + protected volatile SmsChannelProperties properties;
  28 + /**
  29 + * 错误码枚举类
  30 + */
  31 + protected final SmsCodeMapping codeMapping;
  32 +
  33 + /**
  34 + * 短信客户端有参构造函数
  35 + *
  36 + * @param properties 短信配置
  37 + */
  38 + public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
  39 + this.properties = properties;
  40 + this.codeMapping = codeMapping;
  41 + }
  42 +
  43 + /**
  44 + * 初始化
  45 + */
  46 + public final void init() {
  47 + doInit();
  48 + log.info("[init][配置({}) 初始化完成]", properties);
  49 + }
  50 +
  51 + public final void refresh(SmsChannelProperties properties) {
  52 + // 判断是否更新
  53 + if (properties.equals(this.properties)) {
  54 + return;
  55 + }
  56 + log.info("[refresh][配置({})发生变化,重新初始化]", properties);
  57 + this.properties = properties;
  58 + // 初始化
  59 + this.init();
  60 + }
  61 +
  62 + /**
  63 + * 自定义初始化
  64 + */
  65 + protected abstract void doInit();
  66 +
  67 + @Override
  68 + public Long getId() {
  69 + return properties.getId();
  70 + }
  71 +
  72 + @Override
  73 + public final SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile,
  74 + String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
  75 + // 执行短信发送
  76 + SmsCommonResult<SmsSendRespDTO> result;
  77 + try {
  78 + result = doSendSms(logId, mobile, apiTemplateId, templateParams);
  79 + } catch (Throwable ex) {
  80 + // 打印异常日志
  81 + log.error("[sendSms][发送短信异常,sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]",
  82 + logId, mobile, apiTemplateId, templateParams, ex);
  83 + // 封装返回
  84 + return SmsCommonResult.error(ex);
  85 + }
  86 + return result;
  87 + }
  88 +
  89 + protected abstract SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
  90 + String apiTemplateId, List<KeyValue<String, Object>> templateParams)
  91 + throws Throwable;
  92 +
  93 + @Override
  94 + public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable {
  95 + try {
  96 + return doParseSmsReceiveStatus(text);
  97 + } catch (Throwable ex) {
  98 + log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex);
  99 + throw ex;
  100 + }
  101 + }
  102 +
  103 + protected abstract List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable;
  104 +
  105 + @Override
  106 + public SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId) {
  107 + // 执行短信发送
  108 + SmsCommonResult<SmsTemplateRespDTO> result;
  109 + try {
  110 + result = doGetSmsTemplate(apiTemplateId);
  111 + } catch (Throwable ex) {
  112 + // 打印异常日志
  113 + log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex);
  114 + // 封装返回
  115 + return SmsCommonResult.error(ex);
  116 + }
  117 + return result;
  118 + }
  119 +
  120 + protected abstract SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable;
  121 +
  122 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/SmsClientFactoryImpl.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl;
  2 +
  3 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
  4 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
  5 +import cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
  6 +import cn.iocoder.dashboard.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
  7 +import cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian.YunpianSmsClient;
  8 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
  9 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.util.Assert;
  12 +import org.springframework.validation.annotation.Validated;
  13 +
  14 +import java.util.Arrays;
  15 +import java.util.concurrent.ConcurrentHashMap;
  16 +import java.util.concurrent.ConcurrentMap;
  17 +
  18 +/**
  19 + * 短信客户端工厂接口
  20 + *
  21 + * @author zzf
  22 + */
  23 +@Validated
  24 +@Slf4j
  25 +public class SmsClientFactoryImpl implements SmsClientFactory {
  26 +
  27 + /**
  28 + * 短信客户端 Map
  29 + * key:渠道编号,使用 {@link SmsChannelProperties#getId()}
  30 + */
  31 + private final ConcurrentMap<Long, AbstractSmsClient> channelIdClients = new ConcurrentHashMap<>();
  32 +
  33 + /**
  34 + * 短信客户端 Map
  35 + * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()}
  36 + *
  37 + * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。
  38 + * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients}
  39 + */
  40 + private final ConcurrentMap<String, AbstractSmsClient> channelCodeClients = new ConcurrentHashMap<>();
  41 +
  42 + public SmsClientFactoryImpl() {
  43 + // 初始化 channelCodeClients 集合
  44 + Arrays.stream(SmsChannelEnum.values()).forEach(channel -> {
  45 + // 创建一个空的 SmsChannelProperties 对象
  46 + SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode())
  47 + .setApiKey("default").setApiSecret("default");
  48 + // 创建 Sms 客户端
  49 + AbstractSmsClient smsClient = createSmsClient(properties);
  50 + channelCodeClients.put(channel.getCode(), smsClient);
  51 + });
  52 + }
  53 +
  54 + @Override
  55 + public SmsClient getSmsClient(Long channelId) {
  56 + return channelIdClients.get(channelId);
  57 + }
  58 +
  59 + @Override
  60 + public SmsClient getSmsClient(String channelCode) {
  61 + return channelCodeClients.get(channelCode);
  62 + }
  63 +
  64 + @Override
  65 + public void createOrUpdateSmsClient(SmsChannelProperties properties) {
  66 + AbstractSmsClient client = channelIdClients.get(properties.getId());
  67 + if (client == null) {
  68 + client = this.createSmsClient(properties);
  69 + client.init();
  70 + channelIdClients.put(client.getId(), client);
  71 + } else {
  72 + client.refresh(properties);
  73 + }
  74 + }
  75 +
  76 + private AbstractSmsClient createSmsClient(SmsChannelProperties properties) {
  77 + SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode());
  78 + Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum));
  79 + // 创建客户端
  80 + switch (channelEnum) {
  81 + case ALIYUN: return new AliyunSmsClient(properties);
  82 + case YUN_PIAN: return new YunpianSmsClient(properties);
  83 + case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties);
  84 + }
  85 + // 创建失败,错误日志 + 抛出异常
  86 + log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
  87 + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties));
  88 + }
  89 +
  90 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ReflectUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.dashboard.common.core.KeyValue;
  7 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  8 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
  9 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  10 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  11 +import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
  12 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  13 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  14 +import cn.iocoder.dashboard.util.collection.MapUtils;
  15 +import cn.iocoder.dashboard.util.json.JsonUtils;
  16 +import com.aliyuncs.AcsRequest;
  17 +import com.aliyuncs.AcsResponse;
  18 +import com.aliyuncs.DefaultAcsClient;
  19 +import com.aliyuncs.IAcsClient;
  20 +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
  21 +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
  22 +import com.aliyuncs.exceptions.ClientException;
  23 +import com.aliyuncs.profile.DefaultProfile;
  24 +import com.aliyuncs.profile.IClientProfile;
  25 +import com.fasterxml.jackson.annotation.JsonFormat;
  26 +import com.fasterxml.jackson.annotation.JsonProperty;
  27 +import com.google.common.annotations.VisibleForTesting;
  28 +import lombok.Data;
  29 +import lombok.extern.slf4j.Slf4j;
  30 +
  31 +import java.util.Date;
  32 +import java.util.List;
  33 +import java.util.Objects;
  34 +import java.util.function.Function;
  35 +import java.util.stream.Collectors;
  36 +
  37 +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  38 +import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT;
  39 +
  40 +/**
  41 + * 阿里短信客户端的实现类
  42 + *
  43 + * @author zzf
  44 + * @date 2021/1/25 14:17
  45 + */
  46 +@Slf4j
  47 +public class AliyunSmsClient extends AbstractSmsClient {
  48 +
  49 + /**
  50 + * REGION, 使用杭州
  51 + */
  52 + private static final String ENDPOINT = "cn-hangzhou";
  53 +
  54 + /**
  55 + * 阿里云客户端
  56 + */
  57 + private volatile IAcsClient client;
  58 +
  59 + public AliyunSmsClient(SmsChannelProperties properties) {
  60 + super(properties, new AliyunSmsCodeMapping());
  61 + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
  62 + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
  63 + }
  64 +
  65 + @Override
  66 + protected void doInit() {
  67 + IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret());
  68 + client = new DefaultAcsClient(profile);
  69 + }
  70 +
  71 + @Override
  72 + protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
  73 + String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
  74 + // 构建参数
  75 + SendSmsRequest request = new SendSmsRequest();
  76 + request.setPhoneNumbers(mobile);
  77 + request.setSignName(properties.getSignature());
  78 + request.setTemplateCode(apiTemplateId);
  79 + request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
  80 + request.setOutId(String.valueOf(sendLogId));
  81 + // 执行请求
  82 + return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId()));
  83 + }
  84 +
  85 + @Override
  86 + protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
  87 + List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
  88 + return statuses.stream().map(status -> {
  89 + SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
  90 + resp.setSuccess(status.getSuccess());
  91 + resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg());
  92 + resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime());
  93 + resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()));
  94 + return resp;
  95 + }).collect(Collectors.toList());
  96 + }
  97 +
  98 + @Override
  99 + protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
  100 + // 构建参数
  101 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
  102 + request.setTemplateCode(apiTemplateId);
  103 + // 执行请求
  104 + return invoke(request, response -> {
  105 + SmsTemplateRespDTO data = new SmsTemplateRespDTO();
  106 + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
  107 + data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason());
  108 + return data;
  109 + });
  110 + }
  111 +
  112 + @VisibleForTesting
  113 + Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
  114 + switch (templateStatus) {
  115 + case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
  116 + case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
  117 + case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
  118 + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
  119 + }
  120 + }
  121 +
  122 + @VisibleForTesting
  123 + <T extends AcsResponse, R> SmsCommonResult<R> invoke(AcsRequest<T> request, Function<T, R> responseConsumer) {
  124 + try {
  125 + // 执行发送. 由于阿里云 sms 短信没有统一的 Response,但是有统一的 code、message、requestId 属性,所以只好反射
  126 + T sendResult = client.getAcsResponse(request);
  127 + String code = (String) ReflectUtil.getFieldValue(sendResult, "code");
  128 + String message = (String) ReflectUtil.getFieldValue(sendResult, "message");
  129 + String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId");
  130 + // 解析结果
  131 + R data = null;
  132 + if (Objects.equals(code, "OK")) { // 请求成功的情况下
  133 + data = responseConsumer.apply(sendResult);
  134 + }
  135 + // 拼接结果
  136 + return SmsCommonResult.build(code, message, requestId, data, codeMapping);
  137 + } catch (ClientException ex) {
  138 + return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping);
  139 + }
  140 + }
  141 +
  142 + private static String formatResultMsg(ClientException ex) {
  143 + if (StrUtil.isEmpty(ex.getErrorDescription())) {
  144 + return ex.getErrMsg();
  145 + }
  146 + return ex.getErrMsg() + " => " + ex.getErrorDescription();
  147 + }
  148 +
  149 + /**
  150 + * 短信接收状态
  151 + *
  152 + * 参见 https://help.aliyun.com/document_detail/101867.html 文档
  153 + *
  154 + * @author 芋道源码
  155 + */
  156 + @Data
  157 + public static class SmsReceiveStatus {
  158 +
  159 + /**
  160 + * 手机号
  161 + */
  162 + @JsonProperty("phone_number")
  163 + private String phoneNumber;
  164 + /**
  165 + * 发送时间
  166 + */
  167 + @JsonProperty("send_time")
  168 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
  169 + private Date sendTime;
  170 + /**
  171 + * 状态报告时间
  172 + */
  173 + @JsonProperty("report_time")
  174 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
  175 + private Date reportTime;
  176 + /**
  177 + * 是否接收成功
  178 + */
  179 + private Boolean success;
  180 + /**
  181 + * 状态报告说明
  182 + */
  183 + @JsonProperty("err_msg")
  184 + private String errMsg;
  185 + /**
  186 + * 状态报告编码
  187 + */
  188 + @JsonProperty("err_code")
  189 + private String errCode;
  190 + /**
  191 + * 发送序列号
  192 + */
  193 + @JsonProperty("biz_id")
  194 + private String bizId;
  195 + /**
  196 + * 用户序列号
  197 + *
  198 + * 这里我们传递的是 SysSmsLogDO 的日志编号
  199 + */
  200 + @JsonProperty("out_id")
  201 + private String outId;
  202 + /**
  203 + * 短信长度,例如说 1、2、3
  204 + *
  205 + * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送
  206 + */
  207 + @JsonProperty("sms_size")
  208 + private Integer smsSize;
  209 +
  210 + }
  211 +
  212 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
  2 +
  3 +import cn.iocoder.dashboard.common.exception.ErrorCode;
  4 +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
  5 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
  6 +
  7 +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
  8 +
  9 +/**
  10 + * 阿里云的 SmsCodeMapping 实现类
  11 + *
  12 + * 参见 https://help.aliyun.com/document_detail/101346.htm 文档
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +public class AliyunSmsCodeMapping implements SmsCodeMapping {
  17 +
  18 + @Override
  19 + public ErrorCode apply(String apiCode) {
  20 + switch (apiCode) {
  21 + case "OK": return GlobalErrorCodeConstants.SUCCESS;
  22 + case "isv.ACCOUNT_NOT_EXISTS":
  23 + case "isv.ACCOUNT_ABNORMAL":
  24 + case "MissingAccessKeyId": return SMS_ACCOUNT_INVALID;
  25 + case "isp.RAM_PERMISSION_DENY": return SMS_PERMISSION_DENY;
  26 + case "isv.INVALID_JSON_PARAM":
  27 + case "isv.INVALID_PARAMETERS": return SMS_API_PARAM_ERROR;
  28 + case "isv.BUSINESS_LIMIT_CONTROL": return SMS_SEND_BUSINESS_LIMIT_CONTROL;
  29 + case "isv.DAY_LIMIT_CONTROL": return SMS_SEND_DAY_LIMIT_CONTROL;
  30 + case "isv.SMS_CONTENT_ILLEGAL": return SMS_SEND_CONTENT_INVALID;
  31 + case "isv.SMS_TEMPLATE_ILLEGAL": return SMS_TEMPLATE_INVALID;
  32 + case "isv.SMS_SIGNATURE_ILLEGAL":
  33 + case "isv.SIGN_NAME_ILLEGAL":
  34 + case "isv.SMS_SIGN_ILLEGAL": return SMS_SIGN_INVALID;
  35 + case "isv.AMOUNT_NOT_ENOUGH":
  36 + case "isv.OUT_OF_SERVICE": return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
  37 + case "isv.MOBILE_NUMBER_ILLEGAL": return SMS_MOBILE_INVALID;
  38 + case "isv.TEMPLATE_MISSING_PARAMETERS": return SMS_TEMPLATE_PARAM_ERROR;
  39 + }
  40 + return SMS_UNKNOWN;
  41 + }
  42 +
  43 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.debug;
  2 +
  3 +import cn.iocoder.dashboard.common.exception.ErrorCode;
  4 +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
  5 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
  6 +
  7 +import java.util.Objects;
  8 +
  9 +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
  10 +
  11 +/**
  12 + * 钉钉的 SmsCodeMapping 实现类
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +public class DebugDingTalkCodeMapping implements SmsCodeMapping {
  17 +
  18 + @Override
  19 + public ErrorCode apply(String apiCode) {
  20 + return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SMS_UNKNOWN;
  21 + }
  22 +
  23 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.debug;
  2 +
  3 +import cn.hutool.core.codec.Base64;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.map.MapUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.crypto.digest.DigestUtil;
  8 +import cn.hutool.crypto.digest.HmacAlgorithm;
  9 +import cn.hutool.http.HttpUtil;
  10 +import cn.iocoder.dashboard.common.core.KeyValue;
  11 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  12 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
  13 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  14 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  15 +import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
  16 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  17 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  18 +import cn.iocoder.dashboard.util.collection.MapUtils;
  19 +import cn.iocoder.dashboard.util.json.JsonUtils;
  20 +
  21 +import java.util.HashMap;
  22 +import java.util.List;
  23 +import java.util.Map;
  24 +
  25 +/**
  26 + * 基于钉钉 WebHook 实现的调试的短信客户端实现类
  27 + *
  28 + * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +public class DebugDingTalkSmsClient extends AbstractSmsClient {
  33 +
  34 + public DebugDingTalkSmsClient(SmsChannelProperties properties) {
  35 + super(properties, new DebugDingTalkCodeMapping());
  36 + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
  37 + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
  38 + }
  39 +
  40 + @Override
  41 + protected void doInit() {
  42 + }
  43 +
  44 + @Override
  45 + protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
  46 + String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
  47 + // 构建请求
  48 + String url = buildUrl("robot/send");
  49 + Map<String, Object> params = new HashMap<>();
  50 + params.put("msgtype", "text");
  51 + String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s",
  52 + mobile, sendLogId, MapUtils.convertMap(templateParams));
  53 + params.put("text", MapUtil.builder().put("content", content).build());
  54 + // 执行请求
  55 + String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params));
  56 + // 解析结果
  57 + Map<?, ?> responseObj = JsonUtils.parseObject(responseText, Map.class);
  58 + return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"),
  59 + null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping);
  60 + }
  61 +
  62 + /**
  63 + * 构建请求地址
  64 + *
  65 + * 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档
  66 + *
  67 + * @param path 请求路径
  68 + * @return 请求地址
  69 + */
  70 + @SuppressWarnings("SameParameterValue")
  71 + private String buildUrl(String path) {
  72 + // 生成 timestamp
  73 + long timestamp = System.currentTimeMillis();
  74 + // 生成 sign
  75 + String secret = properties.getApiSecret();
  76 + String stringToSign = timestamp + "\n" + secret;
  77 + byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign);
  78 + String sign = Base64.encode(signData);
  79 + // 构建最终 URL
  80 + return String.format("https://oapi.dingtalk.com/%s?access_token=%s&timestamp=%d&sign=%s",
  81 + path, properties.getApiKey(), timestamp, sign);
  82 + }
  83 +
  84 + @Override
  85 + protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
  86 + throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调");
  87 + }
  88 +
  89 + @Override
  90 + protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
  91 + SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("")
  92 + .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason("");
  93 + return SmsCommonResult.build("0", "success", null, data, codeMapping);
  94 + }
  95 +
  96 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClient.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.hutool.core.util.URLUtil;
  7 +import cn.iocoder.dashboard.common.core.KeyValue;
  8 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  9 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
  10 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  11 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  12 +import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
  13 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  14 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  15 +import cn.iocoder.dashboard.util.json.JsonUtils;
  16 +import com.fasterxml.jackson.annotation.JsonFormat;
  17 +import com.fasterxml.jackson.annotation.JsonProperty;
  18 +import com.google.common.annotations.VisibleForTesting;
  19 +import com.yunpian.sdk.YunpianClient;
  20 +import com.yunpian.sdk.constant.YunpianConstant;
  21 +import com.yunpian.sdk.model.Result;
  22 +import com.yunpian.sdk.model.Template;
  23 +import lombok.Data;
  24 +import lombok.extern.slf4j.Slf4j;
  25 +
  26 +import java.util.*;
  27 +import java.util.function.Function;
  28 +import java.util.function.Supplier;
  29 +import java.util.stream.Collectors;
  30 +
  31 +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  32 +import static cn.iocoder.dashboard.util.date.DateUtils.TIME_ZONE_DEFAULT;
  33 +
  34 +/**
  35 + * 云片短信客户端的实现类
  36 + *
  37 + * @author zzf
  38 + * @date 9:48 2021/3/5
  39 + */
  40 +@Slf4j
  41 +public class YunpianSmsClient extends AbstractSmsClient {
  42 +
  43 + /**
  44 + * 云信短信客户端
  45 + */
  46 + private volatile YunpianClient client;
  47 +
  48 + public YunpianSmsClient(SmsChannelProperties properties) {
  49 + super(properties, new YunpianSmsCodeMapping());
  50 + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
  51 + }
  52 +
  53 + @Override
  54 + public void doInit() {
  55 + YunpianClient oldClient = client;
  56 + // 初始化新的客户端
  57 + YunpianClient newClient = new YunpianClient(properties.getApiKey());
  58 + newClient.init();
  59 + this.client = newClient;
  60 + // 销毁老的客户端
  61 + if (oldClient != null) {
  62 + oldClient.close();
  63 + }
  64 + }
  65 +
  66 + @Override
  67 + protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
  68 + String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
  69 + return invoke(() -> {
  70 + Map<String, String> request = new HashMap<>();
  71 + request.put(YunpianConstant.MOBILE, mobile);
  72 + request.put(YunpianConstant.TPL_ID, apiTemplateId);
  73 + request.put(YunpianConstant.TPL_VALUE, formatTplValue(templateParams));
  74 + request.put(YunpianConstant.UID, String.valueOf(sendLogId));
  75 + request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl());
  76 + return client.sms().tpl_single_send(request);
  77 + }, response -> new SmsSendRespDTO().setSerialNo(String.valueOf(response.getSid())));
  78 + }
  79 +
  80 + private static String formatTplValue(List<KeyValue<String, Object>> templateParams) {
  81 + if (CollUtil.isEmpty(templateParams)) {
  82 + return "";
  83 + }
  84 + // 参考 https://www.yunpian.com/official/document/sms/zh_cn/introduction_demos_encode_sample 格式化
  85 + StringJoiner joiner = new StringJoiner("&");
  86 + templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(), URLUtil.encode(String.valueOf(param.getValue())))));
  87 + return joiner.toString();
  88 + }
  89 +
  90 + @Override
  91 + protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
  92 + List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
  93 + return statuses.stream().map(status -> {
  94 + SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
  95 + resp.setSuccess(Objects.equals(status.getReportStatus(), "SUCCESS"));
  96 + resp.setErrorCode(status.getErrorMsg()).setErrorMsg(status.getErrorDetail());
  97 + resp.setMobile(status.getMobile()).setReceiveTime(status.getUserReceiveTime());
  98 + resp.setSerialNo(String.valueOf(status.getSid())).setLogId(status.getUid());
  99 + return resp;
  100 + }).collect(Collectors.toList());
  101 + }
  102 +
  103 + @Override
  104 + protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable {
  105 + return invoke(() -> {
  106 + Map<String, String> request = new HashMap<>();
  107 + request.put(YunpianConstant.APIKEY, properties.getApiKey());
  108 + request.put(YunpianConstant.TPL_ID, apiTemplateId);
  109 + return client.tpl().get(request);
  110 + }, response -> {
  111 + Template template = response.get(0);
  112 + return new SmsTemplateRespDTO().setId(String.valueOf(template.getTpl_id())).setContent(template.getTpl_content())
  113 + .setAuditStatus(convertSmsTemplateAuditStatus(template.getCheck_status())).setAuditReason(template.getReason());
  114 + });
  115 + }
  116 +
  117 + @VisibleForTesting
  118 + Integer convertSmsTemplateAuditStatus(String checkStatus) {
  119 + switch (checkStatus) {
  120 + case "CHECKING": return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
  121 + case "SUCCESS": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
  122 + case "FAIL": return SmsTemplateAuditStatusEnum.FAIL.getStatus();
  123 + default: throw new IllegalArgumentException(String.format("未知审核状态(%s)", checkStatus));
  124 + }
  125 + }
  126 +
  127 + @VisibleForTesting
  128 + <T, R> SmsCommonResult<R> invoke(Supplier<Result<T>> requestConsumer, Function<T, R> responseConsumer) throws Throwable {
  129 + // 执行请求
  130 + Result<T> result = requestConsumer.get();
  131 + if (result.getThrowable() != null) {
  132 + throw result.getThrowable();
  133 + }
  134 + // 解析结果
  135 + R data = null;
  136 + if (result.getData() != null) {
  137 + data = responseConsumer.apply(result.getData());
  138 + }
  139 + // 拼接结果
  140 + return SmsCommonResult.build(String.valueOf(result.getCode()), formatResultMsg(result), null, data, codeMapping);
  141 + }
  142 +
  143 + private static String formatResultMsg(Result<?> sendResult) {
  144 + if (StrUtil.isEmpty(sendResult.getDetail())) {
  145 + return sendResult.getMsg();
  146 + }
  147 + return sendResult.getMsg() + " => " + sendResult.getDetail();
  148 + }
  149 +
  150 + /**
  151 + * 短信接收状态
  152 + *
  153 + * 参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档
  154 + *
  155 + * @author 芋道源码
  156 + */
  157 + @Data
  158 + public static class SmsReceiveStatus {
  159 +
  160 + /**
  161 + * 接收状态
  162 + *
  163 + * 目前仅有 SUCCESS / FAIL,所以使用 Boolean 接收
  164 + */
  165 + @JsonProperty("report_status")
  166 + private String reportStatus;
  167 + /**
  168 + * 接收手机号
  169 + */
  170 + private String mobile;
  171 + /**
  172 + * 运营商返回的代码,如:"DB:0103"
  173 + *
  174 + * 由于不同运营商信息不同,此字段仅供参考;
  175 + */
  176 + @JsonProperty("error_msg")
  177 + private String errorMsg;
  178 + /**
  179 + * 运营商反馈代码的中文解释
  180 + *
  181 + * 默认不推送此字段,如需推送,请联系客服
  182 + */
  183 + @JsonProperty("error_detail")
  184 + private String errorDetail;
  185 + /**
  186 + * 短信编号
  187 + */
  188 + private Long sid;
  189 + /**
  190 + * 用户自定义 id
  191 + *
  192 + * 这里我们传递的是 SysSmsLogDO 的日志编号
  193 + */
  194 + private Long uid;
  195 + /**
  196 + * 用户接收时间
  197 + */
  198 + @JsonProperty("user_receive_time")
  199 + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
  200 + private Date userReceiveTime;
  201 +
  202 + }
  203 +
  204 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMapping.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
  2 +
  3 +import cn.iocoder.dashboard.common.exception.ErrorCode;
  4 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
  5 +
  6 +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
  7 +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
  8 +import static com.yunpian.sdk.constant.Code.*;
  9 +
  10 +/**
  11 + * 云片的 SmsCodeMapping 实现类
  12 + *
  13 + * 参见 https://www.yunpian.com/official/document/sms/zh_CN/returnvalue_common 文档
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public class YunpianSmsCodeMapping implements SmsCodeMapping {
  18 +
  19 + @Override
  20 + public ErrorCode apply(String apiCode) {
  21 + int code = Integer.parseInt(apiCode);
  22 + switch (code) {
  23 + case OK: return SUCCESS;
  24 + case ARGUMENT_MISSING: return SMS_API_PARAM_ERROR;
  25 + case BAD_ARGUMENT_FORMAT: return SMS_TEMPLATE_PARAM_ERROR;
  26 + case TPL_NOT_FOUND:
  27 + case TPL_NOT_VALID: return SMS_TEMPLATE_INVALID;
  28 + case MONEY_NOT_ENOUGH: return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
  29 + case BLACK_WORD: return SMS_SEND_CONTENT_INVALID;
  30 + case DUP_IN_SHORT_TIME:
  31 + case TOO_MANY_TIME_IN_5:
  32 + case DAY_LIMIT_PER_MOBILE:
  33 + case HOUR_LIMIT_PER_MOBILE: return SMS_SEND_BUSINESS_LIMIT_CONTROL;
  34 + case BLACK_PHONE_FILTER: return SMS_MOBILE_BLACK;
  35 + case SIGN_NOT_MATCH:
  36 + case BAD_SIGN_FORMAT:
  37 + case SIGN_NOT_VALID: return SMS_SIGN_INVALID;
  38 + case BAD_API_KEY: return SMS_ACCOUNT_INVALID;
  39 + case API_NOT_ALLOWED: return SMS_PERMISSION_DENY;
  40 + case IP_NOT_ALLOWED: return SMS_IP_DENY;
  41 + }
  42 + return SMS_UNKNOWN;
  43 + }
  44 +
  45 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsChannelEnum.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.enums;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +/**
  8 + * 短信渠道枚举
  9 + *
  10 + * @author zzf
  11 + * @date 2021/1/25 10:56
  12 + */
  13 +@Getter
  14 +@AllArgsConstructor
  15 +public enum SmsChannelEnum {
  16 +
  17 + DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"),
  18 + YUN_PIAN("YUN_PIAN", "云片"),
  19 + ALIYUN("ALIYUN", "阿里云"),
  20 +// TENCENT("TENCENT", "腾讯云"),
  21 +// HUA_WEI("HUA_WEI", "华为云"),
  22 + ;
  23 +
  24 + /**
  25 + * 编码
  26 + */
  27 + private final String code;
  28 + /**
  29 + * 名字
  30 + */
  31 + private final String name;
  32 +
  33 + public static SmsChannelEnum getByCode(String code) {
  34 + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
  35 + }
  36 +
  37 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.enums;
  2 +
  3 +import cn.iocoder.dashboard.common.exception.ErrorCode;
  4 +
  5 +/**
  6 + * 短信框架的错误码枚举
  7 + *
  8 + * 短信框架,使用 2-001-000-000 段
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +public interface SmsFrameworkErrorCodeConstants {
  13 +
  14 + ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析");
  15 +
  16 + // ========== 权限 / 限流等相关 2001000100 ==========
  17 +
  18 + ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限");
  19 + // 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信
  20 + ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信");
  21 +
  22 + // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。
  23 + ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流");
  24 + // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。
  25 + ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流");
  26 +
  27 + ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词");
  28 +
  29 + // ========== 模板相关 2001000200 ==========
  30 + ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在
  31 + ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确");
  32 +
  33 + // ========== 签名相关 2001000300 ==========
  34 + ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用");
  35 +
  36 + // ========== 账户相关 2001000400 ==========
  37 + ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足");
  38 + ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在");
  39 +
  40 + // ========== 其它相关 2001000900 开头 ==========
  41 + ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失");
  42 + ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确");
  43 + ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中");
  44 +
  45 + ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常");
  46 +
  47 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.enums;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +
  6 +/**
  7 + * 短信模板的审核状态枚举
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@AllArgsConstructor
  12 +@Getter
  13 +public enum SmsTemplateAuditStatusEnum {
  14 +
  15 + CHECKING(1),
  16 + SUCCESS(2),
  17 + FAIL(3);
  18 +
  19 + private final Integer status;
  20 +
  21 +}
src/main/java/cn/iocoder/dashboard/framework/sms/core/property/SmsChannelProperties.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.property;
  2 +
  3 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
  4 +import lombok.Data;
  5 +import org.springframework.validation.annotation.Validated;
  6 +
  7 +import javax.validation.constraints.NotEmpty;
  8 +import javax.validation.constraints.NotNull;
  9 +
  10 +/**
  11 + * 短信渠道配置类
  12 + *
  13 + * @author zzf
  14 + * @date 2021/1/25 17:01
  15 + */
  16 +@Data
  17 +@Validated
  18 +public class SmsChannelProperties {
  19 +
  20 + /**
  21 + * 渠道编号
  22 + */
  23 + @NotNull(message = "短信渠道 ID 不能为空")
  24 + private Long id;
  25 + /**
  26 + * 短信签名
  27 + */
  28 + @NotEmpty(message = "短信签名不能为空")
  29 + private String signature;
  30 + /**
  31 + * 渠道编码
  32 + *
  33 + * 枚举 {@link SmsChannelEnum}
  34 + */
  35 + @NotEmpty(message = "渠道编码不能为空")
  36 + private String code;
  37 + /**
  38 + * 短信 API 的账号
  39 + */
  40 + @NotEmpty(message = "短信 API 的账号不能为空")
  41 + private String apiKey;
  42 + /**
  43 + * 短信 API 的秘钥
  44 + */
  45 + @NotEmpty(message = "短信 API 的秘钥不能为空")
  46 + private String apiSecret;
  47 + /**
  48 + * 短信发送回调 URL
  49 + */
  50 + private String callbackUrl;
  51 +
  52 +}
src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java
@@ -3,7 +3,6 @@ package cn.iocoder.dashboard.framework.web.core.handler; @@ -3,7 +3,6 @@ package cn.iocoder.dashboard.framework.web.core.handler;
3 import cn.hutool.core.exceptions.ExceptionUtil; 3 import cn.hutool.core.exceptions.ExceptionUtil;
4 import cn.hutool.core.map.MapUtil; 4 import cn.hutool.core.map.MapUtil;
5 import cn.hutool.extra.servlet.ServletUtil; 5 import cn.hutool.extra.servlet.ServletUtil;
6 -import cn.iocoder.dashboard.common.exception.GlobalException;  
7 import cn.iocoder.dashboard.common.exception.ServiceException; 6 import cn.iocoder.dashboard.common.exception.ServiceException;
8 import cn.iocoder.dashboard.common.pojo.CommonResult; 7 import cn.iocoder.dashboard.common.pojo.CommonResult;
9 import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiErrorLogFrameworkService; 8 import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiErrorLogFrameworkService;
@@ -96,9 +95,6 @@ public class GlobalExceptionHandler { @@ -96,9 +95,6 @@ public class GlobalExceptionHandler {
96 if (ex instanceof AccessDeniedException) { 95 if (ex instanceof AccessDeniedException) {
97 return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); 96 return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
98 } 97 }
99 - if (ex instanceof GlobalException) {  
100 - return globalExceptionHandler(request, (GlobalException) ex);  
101 - }  
102 return defaultExceptionHandler(request, ex); 98 return defaultExceptionHandler(request, ex);
103 } 99 }
104 100
@@ -223,25 +219,6 @@ public class GlobalExceptionHandler { @@ -223,25 +219,6 @@ public class GlobalExceptionHandler {
223 } 219 }
224 220
225 /** 221 /**
226 - * 处理全局异常 ServiceException  
227 - *  
228 - * 例如说,Dubbo 请求超时,调用的 Dubbo 服务系统异常  
229 - */  
230 - @ExceptionHandler(value = GlobalException.class)  
231 - public CommonResult<?> globalExceptionHandler(HttpServletRequest req, GlobalException ex) {  
232 - // 系统异常时,才打印异常日志  
233 - if (INTERNAL_SERVER_ERROR.getCode().equals(ex.getCode())) {  
234 - // 插入异常日志  
235 - this.createExceptionLog(req, ex);  
236 - // 普通全局异常,打印 info 日志即可  
237 - } else {  
238 - log.info("[globalExceptionHandler]", ex);  
239 - }  
240 - // 返回 ERROR CommonResult  
241 - return CommonResult.error(ex);  
242 - }  
243 -  
244 - /**  
245 * 处理系统异常,兜底处理所有的一切 222 * 处理系统异常,兜底处理所有的一切
246 */ 223 */
247 @ExceptionHandler(value = Exception.class) 224 @ExceptionHandler(value = Exception.class)
@@ -250,7 +227,7 @@ public class GlobalExceptionHandler { @@ -250,7 +227,7 @@ public class GlobalExceptionHandler {
250 // 插入异常日志 227 // 插入异常日志
251 this.createExceptionLog(req, ex); 228 this.createExceptionLog(req, ex);
252 // 返回 ERROR CommonResult 229 // 返回 ERROR CommonResult
253 - return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMessage()); 230 + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
254 } 231 }
255 232
256 private void createExceptionLog(HttpServletRequest req, Throwable e) { 233 private void createExceptionLog(HttpServletRequest req, Throwable e) {
src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictDataController.java
@@ -61,7 +61,7 @@ public class SysDictDataController { @@ -61,7 +61,7 @@ public class SysDictDataController {
61 @GetMapping("/list-all-simple") 61 @GetMapping("/list-all-simple")
62 @ApiOperation(value = "获得全部字典数据列表", notes = "一般用于管理后台缓存字典数据在本地") 62 @ApiOperation(value = "获得全部字典数据列表", notes = "一般用于管理后台缓存字典数据在本地")
63 // 无需添加权限认证,因为前端全局都需要 63 // 无需添加权限认证,因为前端全局都需要
64 - public CommonResult<List<SysDictDataSimpleVO>> getSimpleDictDatas() { 64 + public CommonResult<List<SysDictDataSimpleRespVO>> getSimpleDictDatas() {
65 List<SysDictDataDO> list = dictDataService.getDictDatas(); 65 List<SysDictDataDO> list = dictDataService.getDictDatas();
66 return success(SysDictDataConvert.INSTANCE.convertList(list)); 66 return success(SysDictDataConvert.INSTANCE.convertList(list));
67 } 67 }
src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/SysDictTypeController.java
@@ -41,7 +41,7 @@ public class SysDictTypeController { @@ -41,7 +41,7 @@ public class SysDictTypeController {
41 return success(dictTypeId); 41 return success(dictTypeId);
42 } 42 }
43 43
44 - @PostMapping("update") 44 + @PutMapping("/update")
45 @ApiOperation("修改字典类型") 45 @ApiOperation("修改字典类型")
46 @PreAuthorize("@ss.hasPermission('system:dict:update')") 46 @PreAuthorize("@ss.hasPermission('system:dict:update')")
47 public CommonResult<Boolean> updateDictType(@Valid @RequestBody SysDictTypeUpdateReqVO reqVO) { 47 public CommonResult<Boolean> updateDictType(@Valid @RequestBody SysDictTypeUpdateReqVO reqVO) {
src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleVO.java renamed to src/main/java/cn/iocoder/dashboard/modules/system/controller/dict/vo/data/SysDictDataSimpleRespVO.java
@@ -4,9 +4,9 @@ import io.swagger.annotations.ApiModel; @@ -4,9 +4,9 @@ import io.swagger.annotations.ApiModel;
4 import io.swagger.annotations.ApiModelProperty; 4 import io.swagger.annotations.ApiModelProperty;
5 import lombok.Data; 5 import lombok.Data;
6 6
7 -@ApiModel("数据字典精简 VO") 7 +@ApiModel("数据字典精简 Response VO")
8 @Data 8 @Data
9 -public class SysDictDataSimpleVO { 9 +public class SysDictDataSimpleRespVO {
10 10
11 @ApiModelProperty(value = "字典类型", required = true, example = "gender") 11 @ApiModelProperty(value = "字典类型", required = true, example = "gender")
12 private String dictType; 12 private String dictType;
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SmsCallbackController.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms;
  2 +
  3 +import cn.hutool.core.util.URLUtil;
  4 +import cn.hutool.extra.servlet.ServletUtil;
  5 +import cn.iocoder.dashboard.common.pojo.CommonResult;
  6 +import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
  7 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
  8 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
  9 +import io.swagger.annotations.Api;
  10 +import io.swagger.annotations.ApiImplicitParam;
  11 +import io.swagger.annotations.ApiOperation;
  12 +import org.springframework.web.bind.annotation.PostMapping;
  13 +import org.springframework.web.bind.annotation.RequestMapping;
  14 +import org.springframework.web.bind.annotation.RequestParam;
  15 +import org.springframework.web.bind.annotation.RestController;
  16 +
  17 +import javax.annotation.Resource;
  18 +import javax.servlet.http.HttpServletRequest;
  19 +
  20 +import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
  21 +
  22 +@Api(tags = "短信回调")
  23 +@RestController
  24 +@RequestMapping("/system/sms/callback")
  25 +public class SmsCallbackController {
  26 +
  27 + @Resource
  28 + private SysSmsService smsService;
  29 +
  30 + @PostMapping("/sms/yunpian")
  31 + @ApiOperation(value = "云片短信的回调", notes = "参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档")
  32 + @ApiImplicitParam(name = "sms_status", value = "发送状态", required = true, example = "[{具体内容}]", dataTypeClass = Long.class)
  33 + @OperateLog(enable = false)
  34 + public String receiveYunpianSmsStatus(@RequestParam("sms_status") String smsStatus) throws Throwable {
  35 + String text = URLUtil.decode(smsStatus); // decode 解码参数,因为它被 encode
  36 + smsService.receiveSmsStatus(SmsChannelEnum.YUN_PIAN.getCode(), text);
  37 + return "SUCCESS"; // 约定返回 SUCCESS 为成功
  38 + }
  39 +
  40 + @PostMapping("/sms/aliyun")
  41 + @ApiOperation(value = "阿里云短信的回调", notes = "参见 https://help.aliyun.com/document_detail/120998.html 文档")
  42 + @OperateLog(enable = false)
  43 + public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable {
  44 + String text = ServletUtil.getBody(request);
  45 + smsService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text);
  46 + return success(true);
  47 + }
  48 +
  49 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsChannelController.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.CommonResult;
  4 +import cn.iocoder.dashboard.common.pojo.PageResult;
  5 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.*;
  6 +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsChannelConvert;
  7 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
  8 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
  9 +import io.swagger.annotations.Api;
  10 +import io.swagger.annotations.ApiImplicitParam;
  11 +import io.swagger.annotations.ApiOperation;
  12 +import org.springframework.security.access.prepost.PreAuthorize;
  13 +import org.springframework.web.bind.annotation.*;
  14 +
  15 +import javax.annotation.Resource;
  16 +import javax.validation.Valid;
  17 +import java.util.Comparator;
  18 +import java.util.List;
  19 +
  20 +import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
  21 +
  22 +@Api(tags = "短信渠道")
  23 +@RestController
  24 +@RequestMapping("system/sms-channel")
  25 +public class SysSmsChannelController {
  26 +
  27 + @Resource
  28 + private SysSmsChannelService smsChannelService;
  29 +
  30 + @PostMapping("/create")
  31 + @ApiOperation("创建短信渠道")
  32 + @PreAuthorize("@ss.hasPermission('system:sms-channel:create')")
  33 + public CommonResult<Long> createSmsChannel(@Valid @RequestBody SysSmsChannelCreateReqVO createReqVO) {
  34 + return success(smsChannelService.createSmsChannel(createReqVO));
  35 + }
  36 +
  37 + @PutMapping("/update")
  38 + @ApiOperation("更新短信渠道")
  39 + @PreAuthorize("@ss.hasPermission('system:sms-channel:update')")
  40 + public CommonResult<Boolean> updateSmsChannel(@Valid @RequestBody SysSmsChannelUpdateReqVO updateReqVO) {
  41 + smsChannelService.updateSmsChannel(updateReqVO);
  42 + return success(true);
  43 + }
  44 +
  45 + @DeleteMapping("/delete")
  46 + @ApiOperation("删除短信渠道")
  47 + @ApiImplicitParam(name = "id", value = "编号", required = true)
  48 + @PreAuthorize("@ss.hasPermission('system:sms-channel:delete')")
  49 + public CommonResult<Boolean> deleteSmsChannel(@RequestParam("id") Long id) {
  50 + smsChannelService.deleteSmsChannel(id);
  51 + return success(true);
  52 + }
  53 +
  54 + @GetMapping("/get")
  55 + @ApiOperation("获得短信渠道")
  56 + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
  57 + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')")
  58 + public CommonResult<SysSmsChannelRespVO> getSmsChannel(@RequestParam("id") Long id) {
  59 + SysSmsChannelDO smsChannel = smsChannelService.getSmsChannel(id);
  60 + return success(SysSmsChannelConvert.INSTANCE.convert(smsChannel));
  61 + }
  62 +
  63 + @GetMapping("/page")
  64 + @ApiOperation("获得短信渠道分页")
  65 + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')")
  66 + public CommonResult<PageResult<SysSmsChannelRespVO>> getSmsChannelPage(@Valid SysSmsChannelPageReqVO pageVO) {
  67 + PageResult<SysSmsChannelDO> pageResult = smsChannelService.getSmsChannelPage(pageVO);
  68 + return success(SysSmsChannelConvert.INSTANCE.convertPage(pageResult));
  69 + }
  70 +
  71 + @GetMapping("/list-all-simple")
  72 + @ApiOperation(value = "获得短信渠道精简列表", notes = "包含被禁用的短信渠道")
  73 + public CommonResult<List<SysSmsChannelSimpleRespVO>> getSimpleSmsChannels() {
  74 + List<SysSmsChannelDO> list = smsChannelService.getSmsChannelList();
  75 + // 排序后,返回给前端
  76 + list.sort(Comparator.comparing(SysSmsChannelDO::getId));
  77 + return success(SysSmsChannelConvert.INSTANCE.convertList03(list));
  78 + }
  79 +
  80 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsLogController.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.CommonResult;
  4 +import cn.iocoder.dashboard.common.pojo.PageResult;
  5 +import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
  6 +import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
  7 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExcelVO;
  8 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO;
  9 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO;
  10 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogRespVO;
  11 +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsLogConvert;
  12 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
  13 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService;
  14 +import io.swagger.annotations.Api;
  15 +import io.swagger.annotations.ApiOperation;
  16 +import org.springframework.security.access.prepost.PreAuthorize;
  17 +import org.springframework.validation.annotation.Validated;
  18 +import org.springframework.web.bind.annotation.GetMapping;
  19 +import org.springframework.web.bind.annotation.RequestMapping;
  20 +import org.springframework.web.bind.annotation.RestController;
  21 +
  22 +import javax.annotation.Resource;
  23 +import javax.servlet.http.HttpServletResponse;
  24 +import javax.validation.Valid;
  25 +import java.io.IOException;
  26 +import java.util.List;
  27 +
  28 +import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
  29 +import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
  30 +
  31 +@Api(tags = "短信日志")
  32 +@RestController
  33 +@RequestMapping("/system/sms-log")
  34 +@Validated
  35 +public class SysSmsLogController {
  36 +
  37 + @Resource
  38 + private SysSmsLogService smsLogService;
  39 +
  40 + @GetMapping("/page")
  41 + @ApiOperation("获得短信日志分页")
  42 + @PreAuthorize("@ss.hasPermission('system:sms-log:query')")
  43 + public CommonResult<PageResult<SysSmsLogRespVO>> getSmsLogPage(@Valid SysSmsLogPageReqVO pageVO) {
  44 + PageResult<SysSmsLogDO> pageResult = smsLogService.getSmsLogPage(pageVO);
  45 + return success(SysSmsLogConvert.INSTANCE.convertPage(pageResult));
  46 + }
  47 +
  48 + @GetMapping("/export-excel")
  49 + @ApiOperation("导出短信日志 Excel")
  50 + @PreAuthorize("@ss.hasPermission('system:sms-log:export')")
  51 + @OperateLog(type = EXPORT)
  52 + public void exportSmsLogExcel(@Valid SysSmsLogExportReqVO exportReqVO,
  53 + HttpServletResponse response) throws IOException {
  54 + List<SysSmsLogDO> list = smsLogService.getSmsLogList(exportReqVO);
  55 + // 导出 Excel
  56 + List<SysSmsLogExcelVO> datas = SysSmsLogConvert.INSTANCE.convertList02(list);
  57 + ExcelUtils.write(response, "短信日志.xls", "数据", SysSmsLogExcelVO.class, datas);
  58 + }
  59 +
  60 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.http 0 → 100644
  1 +### 请求 /menu/list 接口 => 成功
  2 +POST {{baseUrl}}/system/sms-template/send-sms
  3 +Authorization: Bearer {{token}}
  4 +Content-Type: application/json
  5 +
  6 +{
  7 + "code": "test_01",
  8 + "params": {
  9 + "key01": "value01",
  10 + "key02": "value02"
  11 + }
  12 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/SysSmsTemplateController.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.CommonResult;
  4 +import cn.iocoder.dashboard.common.pojo.PageResult;
  5 +import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
  6 +import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
  7 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.*;
  8 +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsTemplateConvert;
  9 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  10 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
  11 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
  12 +import io.swagger.annotations.Api;
  13 +import io.swagger.annotations.ApiImplicitParam;
  14 +import io.swagger.annotations.ApiOperation;
  15 +import org.springframework.security.access.prepost.PreAuthorize;
  16 +import org.springframework.web.bind.annotation.*;
  17 +
  18 +import javax.annotation.Resource;
  19 +import javax.servlet.http.HttpServletResponse;
  20 +import javax.validation.Valid;
  21 +import java.io.IOException;
  22 +import java.util.List;
  23 +
  24 +import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
  25 +import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
  26 +
  27 +@Api("短信模板")
  28 +@RestController
  29 +@RequestMapping("/system/sms-template")
  30 +public class SysSmsTemplateController {
  31 +
  32 + @Resource
  33 + private SysSmsTemplateService smsTemplateService;
  34 + @Resource
  35 + private SysSmsService smsService;
  36 +
  37 + @PostMapping("/create")
  38 + @ApiOperation("创建短信模板")
  39 + @PreAuthorize("@ss.hasPermission('system:sms-template:create')")
  40 + public CommonResult<Long> createSmsTemplate(@Valid @RequestBody SysSmsTemplateCreateReqVO createReqVO) {
  41 + return success(smsTemplateService.createSmsTemplate(createReqVO));
  42 + }
  43 +
  44 + @PutMapping("/update")
  45 + @ApiOperation("更新短信模板")
  46 + @PreAuthorize("@ss.hasPermission('system:sms-template:update')")
  47 + public CommonResult<Boolean> updateSmsTemplate(@Valid @RequestBody SysSmsTemplateUpdateReqVO updateReqVO) {
  48 + smsTemplateService.updateSmsTemplate(updateReqVO);
  49 + return success(true);
  50 + }
  51 +
  52 + @DeleteMapping("/delete")
  53 + @ApiOperation("删除短信模板")
  54 + @ApiImplicitParam(name = "id", value = "编号", required = true)
  55 + @PreAuthorize("@ss.hasPermission('system:sms-template:delete')")
  56 + public CommonResult<Boolean> deleteSmsTemplate(@RequestParam("id") Long id) {
  57 + smsTemplateService.deleteSmsTemplate(id);
  58 + return success(true);
  59 + }
  60 +
  61 + @GetMapping("/get")
  62 + @ApiOperation("获得短信模板")
  63 + @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
  64 + @PreAuthorize("@ss.hasPermission('system:sms-template:query')")
  65 + public CommonResult<SysSmsTemplateRespVO> getSmsTemplate(@RequestParam("id") Long id) {
  66 + SysSmsTemplateDO smsTemplate = smsTemplateService.getSmsTemplate(id);
  67 + return success(SysSmsTemplateConvert.INSTANCE.convert(smsTemplate));
  68 + }
  69 +
  70 + @GetMapping("/page")
  71 + @ApiOperation("获得短信模板分页")
  72 + @PreAuthorize("@ss.hasPermission('system:sms-template:query')")
  73 + public CommonResult<PageResult<SysSmsTemplateRespVO>> getSmsTemplatePage(@Valid SysSmsTemplatePageReqVO pageVO) {
  74 + PageResult<SysSmsTemplateDO> pageResult = smsTemplateService.getSmsTemplatePage(pageVO);
  75 + return success(SysSmsTemplateConvert.INSTANCE.convertPage(pageResult));
  76 + }
  77 +
  78 + @GetMapping("/export-excel")
  79 + @ApiOperation("导出短信模板 Excel")
  80 + @PreAuthorize("@ss.hasPermission('system:sms-template:export')")
  81 + @OperateLog(type = EXPORT)
  82 + public void exportSmsTemplateExcel(@Valid SysSmsTemplateExportReqVO exportReqVO,
  83 + HttpServletResponse response) throws IOException {
  84 + List<SysSmsTemplateDO> list = smsTemplateService.getSmsTemplateList(exportReqVO);
  85 + // 导出 Excel
  86 + List<SysSmsTemplateExcelVO> datas = SysSmsTemplateConvert.INSTANCE.convertList02(list);
  87 + ExcelUtils.write(response, "短信模板.xls", "数据", SysSmsTemplateExcelVO.class, datas);
  88 + }
  89 +
  90 + @PostMapping("/send-sms")
  91 + @ApiOperation("发送短信")
  92 + @PreAuthorize("@ss.hasPermission('system:sms-template:send-sms')")
  93 + public CommonResult<Long> sendSms(@Valid @RequestBody SysSmsTemplateSendReqVO sendReqVO) {
  94 + return success(smsService.sendSingleSms(sendReqVO.getMobile(), null, null,
  95 + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams()));
  96 + }
  97 +
  98 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelBaseVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
  2 +
  3 +import io.swagger.annotations.ApiModelProperty;
  4 +import lombok.Data;
  5 +import org.hibernate.validator.constraints.URL;
  6 +
  7 +import javax.validation.constraints.NotNull;
  8 +
  9 +/**
  10 +* 短信渠道 Base VO,提供给添加、修改、详细的子 VO 使用
  11 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
  12 +*/
  13 +@Data
  14 +public class SysSmsChannelBaseVO {
  15 +
  16 + @ApiModelProperty(value = "短信签名", required = true, example = "芋道源码")
  17 + @NotNull(message = "短信签名不能为空")
  18 + private String signature;
  19 +
  20 + @ApiModelProperty(value = "启用状态", required = true, example = "1")
  21 + @NotNull(message = "启用状态不能为空")
  22 + private Integer status;
  23 +
  24 + @ApiModelProperty(value = "备注", example = "好吃!")
  25 + private String remark;
  26 +
  27 + @ApiModelProperty(value = "短信 API 的账号", required = true, example = "yudao")
  28 + @NotNull(message = "短信 API 的账号不能为空")
  29 + private String apiKey;
  30 +
  31 + @ApiModelProperty(value = "短信 API 的秘钥", example = "yuanma")
  32 + private String apiSecret;
  33 +
  34 + @ApiModelProperty(value = "短信发送回调 URL", example = "http://www.iocoder.cn")
  35 + @URL(message = "回调 URL 格式不正确")
  36 + private String callbackUrl;
  37 +
  38 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelCreateReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +import javax.validation.constraints.NotNull;
  10 +
  11 +@ApiModel("短信渠道创建 Request VO")
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +@ToString(callSuper = true)
  15 +public class SysSmsChannelCreateReqVO extends SysSmsChannelBaseVO {
  16 +
  17 + @ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类")
  18 + @NotNull(message = "渠道编码不能为空")
  19 + private String code;
  20 +
  21 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelPageReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageParam;
  4 +import io.swagger.annotations.ApiModel;
  5 +import io.swagger.annotations.ApiModelProperty;
  6 +import lombok.Data;
  7 +import lombok.EqualsAndHashCode;
  8 +import lombok.ToString;
  9 +import org.springframework.format.annotation.DateTimeFormat;
  10 +
  11 +import java.util.Date;
  12 +
  13 +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  14 +
  15 +@ApiModel("短信渠道分页 Request VO")
  16 +@Data
  17 +@EqualsAndHashCode(callSuper = true)
  18 +@ToString(callSuper = true)
  19 +public class SysSmsChannelPageReqVO extends PageParam {
  20 +
  21 + @ApiModelProperty(value = "任务状态", example = "1")
  22 + private Integer status;
  23 +
  24 + @ApiModelProperty(value = "短信签名", example = "芋道源码", notes = "模糊匹配")
  25 + private String signature;
  26 +
  27 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  28 + @ApiModelProperty(value = "开始创建时间")
  29 + private Date beginCreateTime;
  30 +
  31 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  32 + @ApiModelProperty(value = "结束创建时间")
  33 + private Date endCreateTime;
  34 +
  35 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelRespVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +import java.util.Date;
  10 +
  11 +@ApiModel("短信渠道 Response VO")
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +@ToString(callSuper = true)
  15 +public class SysSmsChannelRespVO extends SysSmsChannelBaseVO {
  16 +
  17 + @ApiModelProperty(value = "编号", required = true, example = "1024")
  18 + private Long id;
  19 +
  20 + @ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类")
  21 + private String code;
  22 +
  23 + @ApiModelProperty(value = "创建时间", required = true)
  24 + private Date createTime;
  25 +
  26 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelSimpleRespVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +
  7 +import javax.validation.constraints.NotNull;
  8 +
  9 +@ApiModel("短信渠道精简 Response VO")
  10 +@Data
  11 +public class SysSmsChannelSimpleRespVO {
  12 +
  13 + @ApiModelProperty(value = "编号", required = true, example = "1024")
  14 + @NotNull(message = "编号不能为空")
  15 + private Long id;
  16 +
  17 + @ApiModelProperty(value = "短信签名", required = true, example = "芋道源码")
  18 + @NotNull(message = "短信签名不能为空")
  19 + private String signature;
  20 +
  21 + @ApiModelProperty(value = "渠道编码", required = true, example = "YUN_PIAN", notes = "参见 SmsChannelEnum 枚举类")
  22 + private String code;
  23 +
  24 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/channel/SysSmsChannelUpdateReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.channel;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +import javax.validation.constraints.NotNull;
  10 +
  11 +@ApiModel("短信渠道更新 Request VO")
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +@ToString(callSuper = true)
  15 +public class SysSmsChannelUpdateReqVO extends SysSmsChannelBaseVO {
  16 +
  17 + @ApiModelProperty(value = "编号", required = true, example = "1024")
  18 + @NotNull(message = "编号不能为空")
  19 + private Long id;
  20 +
  21 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExcelVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.log;
  2 +
  3 +import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat;
  4 +import cn.iocoder.dashboard.framework.excel.core.convert.DictConvert;
  5 +import cn.iocoder.dashboard.framework.excel.core.convert.JsonConvert;
  6 +import com.alibaba.excel.annotation.ExcelProperty;
  7 +import lombok.Data;
  8 +
  9 +import java.util.Date;
  10 +import java.util.Map;
  11 +
  12 +import static cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum.*;
  13 +
  14 +/**
  15 + * 短信日志 Excel VO
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Data
  20 +public class SysSmsLogExcelVO {
  21 +
  22 + @ExcelProperty("编号")
  23 + private Long id;
  24 +
  25 + @ExcelProperty("短信渠道编号")
  26 + private Long channelId;
  27 +
  28 + @ExcelProperty("短信渠道编码")
  29 + private String channelCode;
  30 +
  31 + @ExcelProperty("模板编号")
  32 + private Long templateId;
  33 +
  34 + @ExcelProperty("模板编码")
  35 + private String templateCode;
  36 +
  37 + @ExcelProperty(value = "短信类型", converter = DictConvert.class)
  38 + @DictFormat(SYS_SMS_TEMPLATE_TYPE)
  39 + private Integer templateType;
  40 +
  41 + @ExcelProperty("短信内容")
  42 + private String templateContent;
  43 +
  44 + @ExcelProperty(value = "短信参数", converter = JsonConvert.class)
  45 + private Map<String, Object> templateParams;
  46 +
  47 + @ExcelProperty("短信 API 的模板编号")
  48 + private String apiTemplateId;
  49 +
  50 + @ExcelProperty("手机号")
  51 + private String mobile;
  52 +
  53 + @ExcelProperty("用户编号")
  54 + private Long userId;
  55 +
  56 + @ExcelProperty(value = "用户类型", converter = DictConvert.class)
  57 + @DictFormat(USER_TYPE)
  58 + private Integer userType;
  59 +
  60 + @ExcelProperty(value = "发送状态", converter = DictConvert.class)
  61 + @DictFormat(SYS_SMS_SEND_STATUS)
  62 + private Integer sendStatus;
  63 +
  64 + @ExcelProperty("发送时间")
  65 + private Date sendTime;
  66 +
  67 + @ExcelProperty("发送结果的编码")
  68 + private Integer sendCode;
  69 +
  70 + @ExcelProperty("发送结果的提示")
  71 + private String sendMsg;
  72 +
  73 + @ExcelProperty("短信 API 发送结果的编码")
  74 + private String apiSendCode;
  75 +
  76 + @ExcelProperty("短信 API 发送失败的提示")
  77 + private String apiSendMsg;
  78 +
  79 + @ExcelProperty("短信 API 发送返回的唯一请求 ID")
  80 + private String apiRequestId;
  81 +
  82 + @ExcelProperty("短信 API 发送返回的序号")
  83 + private String apiSerialNo;
  84 +
  85 + @ExcelProperty(value = "接收状态", converter = DictConvert.class)
  86 + @DictFormat(SYS_SMS_RECEIVE_STATUS)
  87 + private Integer receiveStatus;
  88 +
  89 + @ExcelProperty("接收时间")
  90 + private Date receiveTime;
  91 +
  92 + @ExcelProperty("API 接收结果的编码")
  93 + private String apiReceiveCode;
  94 +
  95 + @ExcelProperty("API 接收结果的说明")
  96 + private String apiReceiveMsg;
  97 +
  98 + @ExcelProperty("创建时间")
  99 + private Date createTime;
  100 +
  101 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogExportReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.log;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +import org.springframework.format.annotation.DateTimeFormat;
  7 +
  8 +import java.util.Date;
  9 +
  10 +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  11 +
  12 +@ApiModel(value = "短信日志 Excel 导出 Request VO", description = "参数和 SysSmsLogPageReqVO 是一致的")
  13 +@Data
  14 +public class SysSmsLogExportReqVO {
  15 +
  16 + @ApiModelProperty(value = "短信渠道编号", example = "10")
  17 + private Long channelId;
  18 +
  19 + @ApiModelProperty(value = "模板编号", example = "20")
  20 + private Long templateId;
  21 +
  22 + @ApiModelProperty(value = "手机号", example = "15601691300")
  23 + private String mobile;
  24 +
  25 + @ApiModelProperty(value = "发送状态", example = "1")
  26 + private Integer sendStatus;
  27 +
  28 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  29 + @ApiModelProperty(value = "开始发送时间")
  30 + private Date beginSendTime;
  31 +
  32 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  33 + @ApiModelProperty(value = "结束发送时间")
  34 + private Date endSendTime;
  35 +
  36 + @ApiModelProperty(value = "接收状态", example = "0")
  37 + private Integer receiveStatus;
  38 +
  39 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  40 + @ApiModelProperty(value = "开始接收时间")
  41 + private Date beginReceiveTime;
  42 +
  43 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  44 + @ApiModelProperty(value = "结束接收时间")
  45 + private Date endReceiveTime;
  46 +
  47 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogPageReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.log;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageParam;
  4 +import io.swagger.annotations.ApiModel;
  5 +import io.swagger.annotations.ApiModelProperty;
  6 +import lombok.Data;
  7 +import lombok.EqualsAndHashCode;
  8 +import lombok.ToString;
  9 +import org.springframework.format.annotation.DateTimeFormat;
  10 +
  11 +import java.util.Date;
  12 +
  13 +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  14 +
  15 +@ApiModel("短信日志分页 Request VO")
  16 +@Data
  17 +@EqualsAndHashCode(callSuper = true)
  18 +@ToString(callSuper = true)
  19 +public class SysSmsLogPageReqVO extends PageParam {
  20 +
  21 + @ApiModelProperty(value = "短信渠道编号", example = "10")
  22 + private Long channelId;
  23 +
  24 + @ApiModelProperty(value = "模板编号", example = "20")
  25 + private Long templateId;
  26 +
  27 + @ApiModelProperty(value = "手机号", example = "15601691300")
  28 + private String mobile;
  29 +
  30 + @ApiModelProperty(value = "发送状态", example = "1", notes = "参见 SysSmsSendStatusEnum 枚举类")
  31 + private Integer sendStatus;
  32 +
  33 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  34 + @ApiModelProperty(value = "开始发送时间")
  35 + private Date beginSendTime;
  36 +
  37 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  38 + @ApiModelProperty(value = "结束发送时间")
  39 + private Date endSendTime;
  40 +
  41 + @ApiModelProperty(value = "接收状态", example = "0", notes = "参见 SysSmsReceiveStatusEnum 枚举类")
  42 + private Integer receiveStatus;
  43 +
  44 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  45 + @ApiModelProperty(value = "开始接收时间")
  46 + private Date beginReceiveTime;
  47 +
  48 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  49 + @ApiModelProperty(value = "结束接收时间")
  50 + private Date endReceiveTime;
  51 +
  52 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/log/SysSmsLogRespVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.log;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +
  7 +import java.util.Date;
  8 +import java.util.Map;
  9 +
  10 +@ApiModel("短信日志 Response VO")
  11 +@Data
  12 +public class SysSmsLogRespVO {
  13 +
  14 + @ApiModelProperty(value = "编号", required = true, example = "1024")
  15 + private Long id;
  16 +
  17 + @ApiModelProperty(value = "短信渠道编号", required = true, example = "10")
  18 + private Long channelId;
  19 +
  20 + @ApiModelProperty(value = "短信渠道编码", required = true, example = "ALIYUN")
  21 + private String channelCode;
  22 +
  23 + @ApiModelProperty(value = "模板编号", required = true, example = "20")
  24 + private Long templateId;
  25 +
  26 + @ApiModelProperty(value = "模板编码", required = true, example = "test-01")
  27 + private String templateCode;
  28 +
  29 + @ApiModelProperty(value = "短信类型", required = true, example = "1")
  30 + private Integer templateType;
  31 +
  32 + @ApiModelProperty(value = "短信内容", required = true, example = "你好,你的验证码是 1024")
  33 + private String templateContent;
  34 +
  35 + @ApiModelProperty(value = "短信参数", required = true, example = "name,code")
  36 + private Map<String, Object> templateParams;
  37 +
  38 + @ApiModelProperty(value = "短信 API 的模板编号", required = true, example = "SMS_207945135")
  39 + private String apiTemplateId;
  40 +
  41 + @ApiModelProperty(value = "手机号", required = true, example = "15601691300")
  42 + private String mobile;
  43 +
  44 + @ApiModelProperty(value = "用户编号", example = "10")
  45 + private Long userId;
  46 +
  47 + @ApiModelProperty(value = "用户类型", example = "1")
  48 + private Integer userType;
  49 +
  50 + @ApiModelProperty(value = "发送状态", required = true, example = "1")
  51 + private Integer sendStatus;
  52 +
  53 + @ApiModelProperty(value = "发送时间")
  54 + private Date sendTime;
  55 +
  56 + @ApiModelProperty(value = "发送结果的编码", example = "0")
  57 + private Integer sendCode;
  58 +
  59 + @ApiModelProperty(value = "发送结果的提示", example = "成功")
  60 + private String sendMsg;
  61 +
  62 + @ApiModelProperty(value = "短信 API 发送结果的编码", example = "SUCCESS")
  63 + private String apiSendCode;
  64 +
  65 + @ApiModelProperty(value = "短信 API 发送失败的提示", example = "成功")
  66 + private String apiSendMsg;
  67 +
  68 + @ApiModelProperty(value = "短信 API 发送返回的唯一请求 ID", example = "3837C6D3-B96F-428C-BBB2-86135D4B5B99")
  69 + private String apiRequestId;
  70 +
  71 + @ApiModelProperty(value = "短信 API 发送返回的序号", example = "62923244790")
  72 + private String apiSerialNo;
  73 +
  74 + @ApiModelProperty(value = "接收状态", required = true, example = "0")
  75 + private Integer receiveStatus;
  76 +
  77 + @ApiModelProperty(value = "接收时间")
  78 + private Date receiveTime;
  79 +
  80 + @ApiModelProperty(value = "API 接收结果的编码", example = "DELIVRD")
  81 + private String apiReceiveCode;
  82 +
  83 + @ApiModelProperty(value = "API 接收结果的说明", example = "用户接收成功")
  84 + private String apiReceiveMsg;
  85 +
  86 + @ApiModelProperty(value = "创建时间", required = true)
  87 + private Date createTime;
  88 +
  89 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateBaseVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
  2 +
  3 +import io.swagger.annotations.ApiModelProperty;
  4 +import lombok.Data;
  5 +
  6 +import javax.validation.constraints.NotNull;
  7 +
  8 +/**
  9 +* 短信模板 Base VO,提供给添加、修改、详细的子 VO 使用
  10 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
  11 +*/
  12 +@Data
  13 +public class SysSmsTemplateBaseVO {
  14 +
  15 + @ApiModelProperty(value = "短信类型", required = true, example = "1", notes = "参见 SysSmsTemplateTypeEnum 枚举类")
  16 + @NotNull(message = "短信类型不能为空")
  17 + private Integer type;
  18 +
  19 + @ApiModelProperty(value = "开启状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类")
  20 + @NotNull(message = "开启状态不能为空")
  21 + private Integer status;
  22 +
  23 + @ApiModelProperty(value = "模板编码", required = true, example = "test_01")
  24 + @NotNull(message = "模板编码不能为空")
  25 + private String code;
  26 +
  27 + @ApiModelProperty(value = "模板名称", required = true, example = "yudao")
  28 + @NotNull(message = "模板名称不能为空")
  29 + private String name;
  30 +
  31 + @ApiModelProperty(value = "模板内容", required = true, example = "你好,{name}。你长的太{like}啦!")
  32 + @NotNull(message = "模板内容不能为空")
  33 + private String content;
  34 +
  35 + @ApiModelProperty(value = "备注", example = "哈哈哈")
  36 + private String remark;
  37 +
  38 + @ApiModelProperty(value = "短信 API 的模板编号", required = true, example = "4383920")
  39 + @NotNull(message = "短信 API 的模板编号不能为空")
  40 + private String apiTemplateId;
  41 +
  42 + @ApiModelProperty(value = "短信渠道编号", required = true, example = "10")
  43 + @NotNull(message = "短信渠道编号不能为空")
  44 + private Long channelId;
  45 +
  46 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateCreateReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import lombok.Data;
  5 +import lombok.EqualsAndHashCode;
  6 +import lombok.ToString;
  7 +
  8 +@ApiModel("短信模板创建 Request VO")
  9 +@Data
  10 +@EqualsAndHashCode(callSuper = true)
  11 +@ToString(callSuper = true)
  12 +public class SysSmsTemplateCreateReqVO extends SysSmsTemplateBaseVO {
  13 +
  14 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExcelVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
  2 +
  3 +import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat;
  4 +import cn.iocoder.dashboard.framework.excel.core.convert.DictConvert;
  5 +import com.alibaba.excel.annotation.ExcelProperty;
  6 +import lombok.Data;
  7 +
  8 +import java.util.Date;
  9 +
  10 +import static cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum.*;
  11 +
  12 +/**
  13 + * 短信模板 Excel VO
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Data
  18 +public class SysSmsTemplateExcelVO {
  19 +
  20 + @ExcelProperty("编号")
  21 + private Long id;
  22 +
  23 + @ExcelProperty(value = "短信签名", converter = DictConvert.class)
  24 + @DictFormat(SYS_SMS_TEMPLATE_TYPE)
  25 + private Integer type;
  26 +
  27 + @ExcelProperty(value = "开启状态", converter = DictConvert.class)
  28 + @DictFormat(SYS_COMMON_STATUS)
  29 + private Integer status;
  30 +
  31 + @ExcelProperty("模板编码")
  32 + private String code;
  33 +
  34 + @ExcelProperty("模板名称")
  35 + private String name;
  36 +
  37 + @ExcelProperty("模板内容")
  38 + private String content;
  39 +
  40 + @ExcelProperty("备注")
  41 + private String remark;
  42 +
  43 + @ExcelProperty("短信 API 的模板编号")
  44 + private String apiTemplateId;
  45 +
  46 + @ExcelProperty("短信渠道编号")
  47 + private Long channelId;
  48 +
  49 + @ExcelProperty(value = "短信渠道编码", converter = DictConvert.class)
  50 + @DictFormat(SYS_SMS_CHANNEL_CODE)
  51 + private String channelCode;
  52 +
  53 + @ExcelProperty("创建时间")
  54 + private Date createTime;
  55 +
  56 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateExportReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +import org.springframework.format.annotation.DateTimeFormat;
  7 +
  8 +import java.util.Date;
  9 +
  10 +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  11 +
  12 +@ApiModel(value = "短信模板 Excel 导出 Request VO", description = "参数和 SysSmsTemplatePageReqVO 是一致的")
  13 +@Data
  14 +public class SysSmsTemplateExportReqVO {
  15 +
  16 + @ApiModelProperty(value = "短信签名", example = "1")
  17 + private Integer type;
  18 +
  19 + @ApiModelProperty(value = "开启状态", example = "1")
  20 + private Integer status;
  21 +
  22 + @ApiModelProperty(value = "模板编码", example = "test_01", notes = "模糊匹配")
  23 + private String code;
  24 +
  25 + @ApiModelProperty(value = "模板内容", example = "你好,{name}。你长的太{like}啦!", notes = "模糊匹配")
  26 + private String content;
  27 +
  28 + @ApiModelProperty(value = "短信 API 的模板编号", example = "4383920", notes = "模糊匹配")
  29 + private String apiTemplateId;
  30 +
  31 + @ApiModelProperty(value = "短信渠道编号", example = "10")
  32 + private Long channelId;
  33 +
  34 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  35 + @ApiModelProperty(value = "开始创建时间")
  36 + private Date beginCreateTime;
  37 +
  38 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  39 + @ApiModelProperty(value = "结束创建时间")
  40 + private Date endCreateTime;
  41 +
  42 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplatePageReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageParam;
  4 +import io.swagger.annotations.ApiModel;
  5 +import io.swagger.annotations.ApiModelProperty;
  6 +import lombok.Data;
  7 +import lombok.EqualsAndHashCode;
  8 +import lombok.ToString;
  9 +import org.springframework.format.annotation.DateTimeFormat;
  10 +
  11 +import java.util.Date;
  12 +
  13 +import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  14 +
  15 +@ApiModel("短信模板分页 Request VO")
  16 +@Data
  17 +@EqualsAndHashCode(callSuper = true)
  18 +@ToString(callSuper = true)
  19 +public class SysSmsTemplatePageReqVO extends PageParam {
  20 +
  21 + @ApiModelProperty(value = "短信签名", example = "1")
  22 + private Integer type;
  23 +
  24 + @ApiModelProperty(value = "开启状态", example = "1")
  25 + private Integer status;
  26 +
  27 + @ApiModelProperty(value = "模板编码", example = "test_01", notes = "模糊匹配")
  28 + private String code;
  29 +
  30 + @ApiModelProperty(value = "模板内容", example = "你好,{name}。你长的太{like}啦!", notes = "模糊匹配")
  31 + private String content;
  32 +
  33 + @ApiModelProperty(value = "短信 API 的模板编号", example = "4383920", notes = "模糊匹配")
  34 + private String apiTemplateId;
  35 +
  36 + @ApiModelProperty(value = "短信渠道编号", example = "10")
  37 + private Long channelId;
  38 +
  39 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  40 + @ApiModelProperty(value = "开始创建时间")
  41 + private Date beginCreateTime;
  42 +
  43 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  44 + @ApiModelProperty(value = "结束创建时间")
  45 + private Date endCreateTime;
  46 +
  47 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateRespVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +import java.util.Date;
  10 +import java.util.List;
  11 +
  12 +@ApiModel("短信模板 Response VO")
  13 +@Data
  14 +@EqualsAndHashCode(callSuper = true)
  15 +@ToString(callSuper = true)
  16 +public class SysSmsTemplateRespVO extends SysSmsTemplateBaseVO {
  17 +
  18 + @ApiModelProperty(value = "编号", required = true, example = "1024")
  19 + private Long id;
  20 +
  21 + @ApiModelProperty(value = "短信渠道编码", required = true, example = "ALIYUN")
  22 + private String channelCode;
  23 +
  24 + @ApiModelProperty(value = "参数数组", example = "name,code")
  25 + private List<String> params;
  26 +
  27 + @ApiModelProperty(value = "创建时间", required = true)
  28 + private Date createTime;
  29 +
  30 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateSendReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +
  7 +import javax.validation.constraints.NotNull;
  8 +import java.util.Map;
  9 +
  10 +@ApiModel("短信模板的发送 Request VO")
  11 +@Data
  12 +public class SysSmsTemplateSendReqVO {
  13 +
  14 + @ApiModelProperty(value = "手机号", required = true, example = "15601691300")
  15 + @NotNull(message = "手机号不能为空")
  16 + private String mobile;
  17 +
  18 + @ApiModelProperty(value = "模板编码", required = true, example = "test_01")
  19 + @NotNull(message = "模板编码不能为空")
  20 + private String templateCode;
  21 +
  22 + @ApiModelProperty(value = "模板参数")
  23 + private Map<String, Object> templateParams;
  24 +
  25 +}
src/main/java/cn/iocoder/dashboard/modules/system/controller/sms/vo/template/SysSmsTemplateUpdateReqVO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.controller.sms.vo.template;
  2 +
  3 +import io.swagger.annotations.ApiModel;
  4 +import io.swagger.annotations.ApiModelProperty;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +import javax.validation.constraints.NotNull;
  10 +
  11 +@ApiModel("短信模板更新 Request VO")
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +@ToString(callSuper = true)
  15 +public class SysSmsTemplateUpdateReqVO extends SysSmsTemplateBaseVO {
  16 +
  17 + @ApiModelProperty(value = "编号", required = true, example = "1024")
  18 + @NotNull(message = "编号不能为空")
  19 + private Long id;
  20 +
  21 +}
src/main/java/cn/iocoder/dashboard/modules/system/convert/dict/SysDictDataConvert.java
@@ -13,7 +13,7 @@ public interface SysDictDataConvert { @@ -13,7 +13,7 @@ public interface SysDictDataConvert {
13 13
14 SysDictDataConvert INSTANCE = Mappers.getMapper(SysDictDataConvert.class); 14 SysDictDataConvert INSTANCE = Mappers.getMapper(SysDictDataConvert.class);
15 15
16 - List<SysDictDataSimpleVO> convertList(List<SysDictDataDO> list); 16 + List<SysDictDataSimpleRespVO> convertList(List<SysDictDataDO> list);
17 17
18 SysDictDataRespVO convert(SysDictDataDO bean); 18 SysDictDataRespVO convert(SysDictDataDO bean);
19 19
src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsChannelConvert.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.convert.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageResult;
  4 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  5 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO;
  6 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelRespVO;
  7 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelSimpleRespVO;
  8 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO;
  9 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
  10 +import org.mapstruct.Mapper;
  11 +import org.mapstruct.factory.Mappers;
  12 +
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * 短信渠道 Convert
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@Mapper
  21 +public interface SysSmsChannelConvert {
  22 +
  23 + SysSmsChannelConvert INSTANCE = Mappers.getMapper(SysSmsChannelConvert.class);
  24 +
  25 + SysSmsChannelDO convert(SysSmsChannelCreateReqVO bean);
  26 +
  27 + SysSmsChannelDO convert(SysSmsChannelUpdateReqVO bean);
  28 +
  29 + SysSmsChannelRespVO convert(SysSmsChannelDO bean);
  30 +
  31 + List<SysSmsChannelRespVO> convertList(List<SysSmsChannelDO> list);
  32 +
  33 + PageResult<SysSmsChannelRespVO> convertPage(PageResult<SysSmsChannelDO> page);
  34 +
  35 + List<SmsChannelProperties> convertList02(List<SysSmsChannelDO> list);
  36 +
  37 + List<SysSmsChannelSimpleRespVO> convertList03(List<SysSmsChannelDO> list);
  38 +
  39 +}
src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsLogConvert.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.convert.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageResult;
  4 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExcelVO;
  5 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogRespVO;
  6 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
  7 +import org.mapstruct.Mapper;
  8 +import org.mapstruct.factory.Mappers;
  9 +
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * 短信日志 Convert
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface SysSmsLogConvert {
  19 +
  20 + SysSmsLogConvert INSTANCE = Mappers.getMapper(SysSmsLogConvert.class);
  21 +
  22 + SysSmsLogRespVO convert(SysSmsLogDO bean);
  23 +
  24 + List<SysSmsLogRespVO> convertList(List<SysSmsLogDO> list);
  25 +
  26 + PageResult<SysSmsLogRespVO> convertPage(PageResult<SysSmsLogDO> page);
  27 +
  28 + List<SysSmsLogExcelVO> convertList02(List<SysSmsLogDO> list);
  29 +
  30 +}
src/main/java/cn/iocoder/dashboard/modules/system/convert/sms/SysSmsTemplateConvert.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.convert.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageResult;
  4 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO;
  5 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExcelVO;
  6 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateRespVO;
  7 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO;
  8 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  9 +import org.mapstruct.Mapper;
  10 +import org.mapstruct.factory.Mappers;
  11 +
  12 +import java.util.List;
  13 +
  14 +@Mapper
  15 +public interface SysSmsTemplateConvert {
  16 +
  17 + SysSmsTemplateConvert INSTANCE = Mappers.getMapper(SysSmsTemplateConvert.class);
  18 +
  19 + SysSmsTemplateDO convert(SysSmsTemplateCreateReqVO bean);
  20 +
  21 + SysSmsTemplateDO convert(SysSmsTemplateUpdateReqVO bean);
  22 +
  23 + SysSmsTemplateRespVO convert(SysSmsTemplateDO bean);
  24 +
  25 + List<SysSmsTemplateRespVO> convertList(List<SysSmsTemplateDO> list);
  26 +
  27 + PageResult<SysSmsTemplateRespVO> convertPage(PageResult<SysSmsTemplateDO> page);
  28 +
  29 + List<SysSmsTemplateExcelVO> convertList02(List<SysSmsTemplateDO> list);
  30 +
  31 +}
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsChannelDO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.dal.dataobject.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
  6 +import com.baomidou.mybatisplus.annotation.TableName;
  7 +import lombok.Data;
  8 +import lombok.EqualsAndHashCode;
  9 +import lombok.ToString;
  10 +
  11 +/**
  12 + * 短信渠道 DO
  13 + *
  14 + * @author zzf
  15 + * @since 2021-01-25
  16 + */
  17 +@TableName(value = "sys_sms_channel", autoResultMap = true)
  18 +@Data
  19 +@EqualsAndHashCode(callSuper = true)
  20 +@ToString(callSuper = true)
  21 +public class SysSmsChannelDO extends BaseDO {
  22 +
  23 + /**
  24 + * 渠道编号
  25 + */
  26 + private Long id;
  27 + /**
  28 + * 短信签名
  29 + */
  30 + private String signature;
  31 + /**
  32 + * 渠道编码
  33 + *
  34 + * 枚举 {@link SmsChannelEnum}
  35 + */
  36 + private String code;
  37 + /**
  38 + * 启用状态
  39 + *
  40 + * 枚举 {@link CommonStatusEnum}
  41 + */
  42 + private Integer status;
  43 + /**
  44 + * 备注
  45 + */
  46 + private String remark;
  47 + /**
  48 + * 短信 API 的账号
  49 + */
  50 + private String apiKey;
  51 + /**
  52 + * 短信 API 的秘钥
  53 + */
  54 + private String apiSecret;
  55 + /**
  56 + * 短信发送回调 URL
  57 + */
  58 + private String callbackUrl;
  59 +
  60 +}
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsLogDO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.dal.dataobject.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.enums.UserTypeEnum;
  4 +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
  6 +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsReceiveStatusEnum;
  7 +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum;
  8 +import com.baomidou.mybatisplus.annotation.TableField;
  9 +import com.baomidou.mybatisplus.annotation.TableName;
  10 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
  11 +import lombok.*;
  12 +
  13 +import java.util.Date;
  14 +import java.util.Map;
  15 +
  16 +/**
  17 + * 短信日志 DO
  18 + *
  19 + * @author zzf
  20 + * @since 2021-01-25
  21 + */
  22 +@TableName(value = "sys_sms_log", autoResultMap = true)
  23 +@Data
  24 +@EqualsAndHashCode(callSuper = true)
  25 +@ToString(callSuper = true)
  26 +@Builder
  27 +@AllArgsConstructor
  28 +@NoArgsConstructor
  29 +public class SysSmsLogDO extends BaseDO {
  30 +
  31 + /**
  32 + * 自增编号
  33 + */
  34 + private Long id;
  35 +
  36 + // ========= 渠道相关字段 =========
  37 +
  38 + /**
  39 + * 短信渠道编号
  40 + *
  41 + * 关联 {@link SysSmsChannelDO#getId()}
  42 + */
  43 + private Long channelId;
  44 + /**
  45 + * 短信渠道编码
  46 + *
  47 + * 冗余 {@link SysSmsChannelDO#getCode()}
  48 + */
  49 + private String channelCode;
  50 +
  51 + // ========= 模板相关字段 =========
  52 +
  53 + /**
  54 + * 模板编号
  55 + *
  56 + * 关联 {@link SysSmsTemplateDO#getId()}
  57 + */
  58 + private Long templateId;
  59 + /**
  60 + * 模板编码
  61 + *
  62 + * 冗余 {@link SysSmsTemplateDO#getCode()}
  63 + */
  64 + private String templateCode;
  65 + /**
  66 + * 短信类型
  67 + *
  68 + * 冗余 {@link SysSmsTemplateDO#getType()}
  69 + */
  70 + private Integer templateType;
  71 + /**
  72 + * 基于 {@link SysSmsTemplateDO#getContent()} 格式化后的内容
  73 + */
  74 + private String templateContent;
  75 + /**
  76 + * 基于 {@link SysSmsTemplateDO#getParams()} 输入后的参数
  77 + */
  78 + @TableField(typeHandler = JacksonTypeHandler.class)
  79 + private Map<String, Object> templateParams;
  80 + /**
  81 + * 短信 API 的模板编号
  82 + *
  83 + * 冗余 {@link SysSmsTemplateDO#getApiTemplateId()}
  84 + */
  85 + private String apiTemplateId;
  86 +
  87 + // ========= 手机相关字段 =========
  88 +
  89 + /**
  90 + * 手机号
  91 + */
  92 + private String mobile;
  93 + /**
  94 + * 用户编号
  95 + */
  96 + private Long userId;
  97 + /**
  98 + * 用户类型
  99 + *
  100 + * 枚举 {@link UserTypeEnum}
  101 + */
  102 + private Integer userType;
  103 +
  104 + // ========= 发送相关字段 =========
  105 +
  106 + /**
  107 + * 发送状态
  108 + *
  109 + * 枚举 {@link SysSmsSendStatusEnum}
  110 + */
  111 + private Integer sendStatus;
  112 + /**
  113 + * 发送时间
  114 + */
  115 + private Date sendTime;
  116 + /**
  117 + * 发送结果的编码
  118 + *
  119 + * 枚举 {@link SmsFrameworkErrorCodeConstants}
  120 + */
  121 + private Integer sendCode;
  122 + /**
  123 + * 发送结果的提示
  124 + *
  125 + * 一般情况下,使用 {@link SmsFrameworkErrorCodeConstants}
  126 + * 异常情况下,通过格式化 Exception 的提示存储
  127 + */
  128 + private String sendMsg;
  129 + /**
  130 + * 短信 API 发送结果的编码
  131 + *
  132 + * 由于第三方的错误码可能是字符串,所以使用 String 类型
  133 + */
  134 + private String apiSendCode;
  135 + /**
  136 + * 短信 API 发送失败的提示
  137 + */
  138 + private String apiSendMsg;
  139 + /**
  140 + * 短信 API 发送返回的唯一请求 ID
  141 + *
  142 + * 用于和短信 API 进行定位于排错
  143 + */
  144 + private String apiRequestId;
  145 + /**
  146 + * 短信 API 发送返回的序号
  147 + *
  148 + * 用于和短信 API 平台的发送记录关联
  149 + */
  150 + private String apiSerialNo;
  151 +
  152 + // ========= 接收相关字段 =========
  153 +
  154 + /**
  155 + * 接收状态
  156 + *
  157 + * 枚举 {@link SysSmsReceiveStatusEnum}
  158 + */
  159 + private Integer receiveStatus;
  160 + /**
  161 + * 接收时间
  162 + */
  163 + private Date receiveTime;
  164 + /**
  165 + * 短信 API 接收结果的编码
  166 + */
  167 + private String apiReceiveCode;
  168 + /**
  169 + * 短信 API 接收结果的提示
  170 + */
  171 + private String apiReceiveMsg;
  172 +
  173 +}
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/sms/SysSmsTemplateDO.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.dal.dataobject.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsTemplateTypeEnum;
  6 +import com.baomidou.mybatisplus.annotation.TableField;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
  9 +import lombok.Data;
  10 +import lombok.EqualsAndHashCode;
  11 +import lombok.ToString;
  12 +
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * 短信模板 DO
  17 + *
  18 + * @author zzf
  19 + * @since 2021-01-25
  20 + */
  21 +@TableName(value = "sys_sms_template", autoResultMap = true)
  22 +@Data
  23 +@EqualsAndHashCode(callSuper = true)
  24 +@ToString(callSuper = true)
  25 +public class SysSmsTemplateDO extends BaseDO {
  26 +
  27 + /**
  28 + * 自增编号
  29 + */
  30 + private Long id;
  31 +
  32 + // ========= 模板相关字段 =========
  33 +
  34 + /**
  35 + * 短信类型
  36 + *
  37 + * 枚举 {@link SysSmsTemplateTypeEnum}
  38 + */
  39 + private Integer type;
  40 + /**
  41 + * 启用状态
  42 + *
  43 + * 枚举 {@link CommonStatusEnum}
  44 + */
  45 + private Integer status;
  46 + /**
  47 + * 模板编码,保证唯一
  48 + */
  49 + private String code;
  50 + /**
  51 + * 模板名称
  52 + */
  53 + private String name;
  54 + /**
  55 + * 模板内容
  56 + *
  57 + * 内容的参数,使用 {} 包括,例如说 {name}
  58 + */
  59 + private String content;
  60 + /**
  61 + * 参数数组(自动根据内容生成)
  62 + */
  63 + @TableField(typeHandler = JacksonTypeHandler.class)
  64 + private List<String> params;
  65 + /**
  66 + * 备注
  67 + */
  68 + private String remark;
  69 + /**
  70 + * 短信 API 的模板编号
  71 + */
  72 + private String apiTemplateId;
  73 +
  74 + // ========= 渠道相关字段 =========
  75 +
  76 + /**
  77 + * 短信渠道编号
  78 + *
  79 + * 关联 {@link SysSmsChannelDO#getId()}
  80 + */
  81 + private Long channelId;
  82 + /**
  83 + * 短信渠道编码
  84 + *
  85 + * 冗余 {@link SysSmsChannelDO#getCode()}
  86 + */
  87 + private String channelCode;
  88 +
  89 +}
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsChannelMapper.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.dal.mysql.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageResult;
  4 +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
  6 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO;
  7 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
  8 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +import org.apache.ibatis.annotations.Select;
  11 +
  12 +import java.util.Date;
  13 +import java.util.List;
  14 +
  15 +@Mapper
  16 +public interface SysSmsChannelMapper extends BaseMapperX<SysSmsChannelDO> {
  17 +
  18 + default PageResult<SysSmsChannelDO> selectPage(SysSmsChannelPageReqVO reqVO) {
  19 + return selectPage(reqVO, new QueryWrapperX<SysSmsChannelDO>()
  20 + .likeIfPresent("signature", reqVO.getSignature())
  21 + .eqIfPresent("status", reqVO.getStatus())
  22 + .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
  23 + .orderByDesc("id"));
  24 + }
  25 +
  26 + @Select("SELECT id FROM sys_sms_channel WHERE update_time > #{maxUpdateTime} LIMIT 1")
  27 + Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
  28 +
  29 +}
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsLogMapper.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.dal.mysql.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageResult;
  4 +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
  6 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO;
  7 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO;
  8 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +
  11 +import java.util.List;
  12 +
  13 +@Mapper
  14 +public interface SysSmsLogMapper extends BaseMapperX<SysSmsLogDO> {
  15 +
  16 + default PageResult<SysSmsLogDO> selectPage(SysSmsLogPageReqVO reqVO) {
  17 + return selectPage(reqVO, new QueryWrapperX<SysSmsLogDO>()
  18 + .eqIfPresent("channel_id", reqVO.getChannelId())
  19 + .eqIfPresent("template_id", reqVO.getTemplateId())
  20 + .likeIfPresent("mobile", reqVO.getMobile())
  21 + .eqIfPresent("send_status", reqVO.getSendStatus())
  22 + .betweenIfPresent("send_time", reqVO.getBeginSendTime(), reqVO.getEndSendTime())
  23 + .eqIfPresent("receive_status", reqVO.getReceiveStatus())
  24 + .betweenIfPresent("receive_time", reqVO.getBeginReceiveTime(), reqVO.getEndReceiveTime())
  25 + .orderByDesc("id"));
  26 + }
  27 +
  28 + default List<SysSmsLogDO> selectList(SysSmsLogExportReqVO reqVO) {
  29 + return selectList(new QueryWrapperX<SysSmsLogDO>()
  30 + .eqIfPresent("channel_id", reqVO.getChannelId())
  31 + .eqIfPresent("template_id", reqVO.getTemplateId())
  32 + .likeIfPresent("mobile", reqVO.getMobile())
  33 + .eqIfPresent("send_status", reqVO.getSendStatus())
  34 + .betweenIfPresent("send_time", reqVO.getBeginSendTime(), reqVO.getEndSendTime())
  35 + .eqIfPresent("receive_status", reqVO.getReceiveStatus())
  36 + .betweenIfPresent("receive_time", reqVO.getBeginReceiveTime(), reqVO.getEndReceiveTime())
  37 + .orderByDesc("id"));
  38 + }
  39 +
  40 +}
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/sms/SysSmsTemplateMapper.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.dal.mysql.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageResult;
  4 +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
  6 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO;
  7 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO;
  8 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +import org.apache.ibatis.annotations.Select;
  11 +
  12 +import java.util.Date;
  13 +import java.util.List;
  14 +
  15 +@Mapper
  16 +public interface SysSmsTemplateMapper extends BaseMapperX<SysSmsTemplateDO> {
  17 +
  18 + default SysSmsTemplateDO selectByCode(String code) {
  19 + return selectOne("code", code);
  20 + }
  21 +
  22 + default PageResult<SysSmsTemplateDO> selectPage(SysSmsTemplatePageReqVO reqVO) {
  23 + return selectPage(reqVO, new QueryWrapperX<SysSmsTemplateDO>()
  24 + .eqIfPresent("type", reqVO.getType())
  25 + .eqIfPresent("status", reqVO.getStatus())
  26 + .likeIfPresent("code", reqVO.getCode())
  27 + .likeIfPresent("content", reqVO.getContent())
  28 + .likeIfPresent("api_template_id", reqVO.getApiTemplateId())
  29 + .eqIfPresent("channel_id", reqVO.getChannelId())
  30 + .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
  31 + .orderByDesc("id"));
  32 + }
  33 +
  34 + default List<SysSmsTemplateDO> selectList(SysSmsTemplateExportReqVO reqVO) {
  35 + return selectList(new QueryWrapperX<SysSmsTemplateDO>()
  36 + .eqIfPresent("type", reqVO.getType())
  37 + .eqIfPresent("status", reqVO.getStatus())
  38 + .likeIfPresent("code", reqVO.getCode())
  39 + .likeIfPresent("content", reqVO.getContent())
  40 + .likeIfPresent("api_template_id", reqVO.getApiTemplateId())
  41 + .eqIfPresent("channel_id", reqVO.getChannelId())
  42 + .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
  43 + .orderByDesc("id"));
  44 + }
  45 +
  46 + default Integer selectCountByChannelId(Long channelId) {
  47 + return selectCount("channel_id", channelId);
  48 + }
  49 +
  50 + @Select("SELECT id FROM sys_sms_template WHERE update_time > #{maxUpdateTime} LIMIT 1")
  51 + Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
  52 +
  53 +}
src/main/java/cn/iocoder/dashboard/modules/system/dal/redis/RedisKeyConstants.java 0 → 100644
src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java
@@ -30,8 +30,8 @@ public interface SysErrorCodeConstants { @@ -30,8 +30,8 @@ public interface SysErrorCodeConstants {
30 30
31 // ========== 角色模块 1002003000 ========== 31 // ========== 角色模块 1002003000 ==========
32 ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1002003000, "角色不存在"); 32 ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1002003000, "角色不存在");
33 - ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1002003001, "已经存在名为【{}}】的角色");  
34 - ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1002003002, "已经存在编码为【{}}】的角色"); 33 + ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1002003001, "已经存在名为【{}】的角色");
  34 + ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1002003002, "已经存在编码为【{}】的角色");
35 ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1002003004, "不能操作类型为系统内置的角色"); 35 ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1002003004, "不能操作类型为系统内置的角色");
36 36
37 // ========== 用户模块 1002004000 ========== 37 // ========== 用户模块 1002004000 ==========
@@ -78,4 +78,18 @@ public interface SysErrorCodeConstants { @@ -78,4 +78,18 @@ public interface SysErrorCodeConstants {
78 ErrorCode FILE_UPLOAD_FAILED = new ErrorCode(1002009002, "文件上传失败"); 78 ErrorCode FILE_UPLOAD_FAILED = new ErrorCode(1002009002, "文件上传失败");
79 ErrorCode FILE_IS_EMPTY= new ErrorCode(1002009003, "文件为空"); 79 ErrorCode FILE_IS_EMPTY= new ErrorCode(1002009003, "文件为空");
80 80
  81 + // ========== 短信渠道 1002011000 ==========
  82 + ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1002011000, "短信渠道不存在");
  83 + ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1002011001, "短信渠道不处于开启状态,不允许选择");
  84 + ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1002011002, "无法删除,该短信渠道还有短信模板");
  85 +
  86 + // ========== 短信模板 1002011000 ==========
  87 + ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1002011000, "短信模板不存在");
  88 + ErrorCode SMS_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1002011001, "已经存在编码为【{}】的短信模板");
  89 +
  90 + // ========== 短信发送 1002012000 ==========
  91 + ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1002012000, "手机号不存在");
  92 + ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1002012001, "模板参数({})缺失");
  93 +
  94 +
81 } 95 }
src/main/java/cn/iocoder/dashboard/modules/system/enums/dict/SysDictTypeEnum.java
@@ -18,6 +18,10 @@ public enum SysDictTypeEnum { @@ -18,6 +18,10 @@ public enum SysDictTypeEnum {
18 SYS_LOGIN_RESULT("sys_login_result"), // 登陆结果 18 SYS_LOGIN_RESULT("sys_login_result"), // 登陆结果
19 SYS_CONFIG_TYPE("sys_config_type"), // 参数配置类型 19 SYS_CONFIG_TYPE("sys_config_type"), // 参数配置类型
20 SYS_BOOLEAN_STRING("sys_boolean_string"), // Boolean 是否类型 20 SYS_BOOLEAN_STRING("sys_boolean_string"), // Boolean 是否类型
  21 + SYS_SMS_CHANNEL_CODE("sys_sms_channel_code"), // 短信渠道编码
  22 + SYS_SMS_TEMPLATE_TYPE("sys_sms_template_type"), // 短信模板类型
  23 + SYS_SMS_SEND_STATUS("sys_sms_send_status"), // 短信发送状态
  24 + SYS_SMS_RECEIVE_STATUS("sys_sms_receive_status"), // 短信接收状态
21 25
22 INF_REDIS_TIMEOUT_TYPE("inf_redis_timeout_type"), // Redis 超时类型 26 INF_REDIS_TIMEOUT_TYPE("inf_redis_timeout_type"), // Redis 超时类型
23 INF_JOB_STATUS("inf_job_status"), // 定时任务状态的枚举 27 INF_JOB_STATUS("inf_job_status"), // 定时任务状态的枚举
src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsReceiveStatusEnum.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.enums.sms;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +
  6 +/**
  7 + * 短信的接收状态枚举
  8 + *
  9 + * @author 芋道源码
  10 + * @date 2021/2/1 13:39
  11 + */
  12 +@Getter
  13 +@AllArgsConstructor
  14 +public enum SysSmsReceiveStatusEnum {
  15 +
  16 + INIT(0), // 初始化
  17 + SUCCESS(10), // 接收成功
  18 + FAILURE(20), // 接收失败
  19 + ;
  20 +
  21 + private final int status;
  22 +
  23 +}
src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsSendStatusEnum.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.enums.sms;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +
  6 +/**
  7 + * 短信的发送状态枚举
  8 + *
  9 + * @author zzf
  10 + * @date 2021/2/1 13:39
  11 + */
  12 +@Getter
  13 +@AllArgsConstructor
  14 +public enum SysSmsSendStatusEnum {
  15 +
  16 + INIT(0), // 初始化
  17 + SUCCESS(10), // 发送成功
  18 + FAILURE(20), // 发送失败
  19 + IGNORE(30), // 忽略,即不发送
  20 + ;
  21 +
  22 + private final int status;
  23 +
  24 +}
src/main/java/cn/iocoder/dashboard/modules/system/enums/sms/SysSmsTemplateTypeEnum.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.enums.sms;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +
  6 +/**
  7 + * 短信的模板类型枚举
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@Getter
  12 +@AllArgsConstructor
  13 +public enum SysSmsTemplateTypeEnum {
  14 +
  15 + VERIFICATION_CODE(1), // 验证码
  16 + NOTICE(2), // 通知
  17 + PROMOTION(3), // 营销
  18 + ;
  19 +
  20 + /**
  21 + * 类型
  22 + */
  23 + private final int type;
  24 +
  25 +}
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsChannelRefreshConsumer.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
  4 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsChannelRefreshMessage;
  5 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import javax.annotation.Resource;
  10 +
  11 +/**
  12 + * 针对 {@link SysSmsChannelRefreshMessage} 的消费者
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Component
  17 +@Slf4j
  18 +public class SysSmsChannelRefreshConsumer extends AbstractChannelMessageListener<SysSmsChannelRefreshMessage> {
  19 +
  20 + @Resource
  21 + private SysSmsChannelService smsChannelService;
  22 +
  23 + @Override
  24 + public void onMessage(SysSmsChannelRefreshMessage message) {
  25 + log.info("[onMessage][收到 SmsChannel 刷新消息]");
  26 + smsChannelService.initSmsClients();
  27 + }
  28 +
  29 +}
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsSendConsumer.java
@@ -2,16 +2,29 @@ package cn.iocoder.dashboard.modules.system.mq.consumer.sms; @@ -2,16 +2,29 @@ package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
2 2
3 import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener; 3 import cn.iocoder.dashboard.framework.redis.core.stream.AbstractStreamMessageListener;
4 import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage; 4 import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
  5 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
5 import lombok.extern.slf4j.Slf4j; 6 import lombok.extern.slf4j.Slf4j;
6 import org.springframework.stereotype.Component; 7 import org.springframework.stereotype.Component;
7 8
  9 +import javax.annotation.Resource;
  10 +
  11 +/**
  12 + * 针对 {@link SysSmsSendMessage} 的消费者
  13 + *
  14 + * @author zzf
  15 + * @date 2021/3/9 16:35
  16 + */
8 @Component 17 @Component
9 @Slf4j 18 @Slf4j
10 public class SysSmsSendConsumer extends AbstractStreamMessageListener<SysSmsSendMessage> { 19 public class SysSmsSendConsumer extends AbstractStreamMessageListener<SysSmsSendMessage> {
11 20
  21 + @Resource
  22 + private SysSmsService smsService;
  23 +
12 @Override 24 @Override
13 public void onMessage(SysSmsSendMessage message) { 25 public void onMessage(SysSmsSendMessage message) {
14 log.info("[onMessage][消息内容({})]", message); 26 log.info("[onMessage][消息内容({})]", message);
  27 + smsService.doSendSms(message);
15 } 28 }
16 29
17 } 30 }
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/sms/SysSmsTemplateRefreshConsumer.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.consumer.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
  4 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsTemplateRefreshMessage;
  5 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import javax.annotation.Resource;
  10 +
  11 +/**
  12 + * 针对 {@link SysSmsTemplateRefreshMessage} 的消费者
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Component
  17 +@Slf4j
  18 +public class SysSmsTemplateRefreshConsumer extends AbstractChannelMessageListener<SysSmsTemplateRefreshMessage> {
  19 +
  20 + @Resource
  21 + private SysSmsTemplateService smsTemplateService;
  22 +
  23 + @Override
  24 + public void onMessage(SysSmsTemplateRefreshMessage message) {
  25 + log.info("[onMessage][收到 SmsTemplate 刷新消息]");
  26 + smsTemplateService.initLocalCache();
  27 + }
  28 +
  29 +}
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsChannelRefreshMessage.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.message.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 短信渠道的数据刷新 Message
  8 + */
  9 +@Data
  10 +public class SysSmsChannelRefreshMessage implements ChannelMessage {
  11 +
  12 + @Override
  13 + public String getChannel() {
  14 + return "system.sms-channel.refresh";
  15 + }
  16 +
  17 +}
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsSendMessage.java
1 package cn.iocoder.dashboard.modules.system.mq.message.sms; 1 package cn.iocoder.dashboard.modules.system.mq.message.sms;
2 2
  3 +import cn.iocoder.dashboard.common.core.KeyValue;
3 import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage; 4 import cn.iocoder.dashboard.framework.redis.core.stream.StreamMessage;
4 import lombok.Data; 5 import lombok.Data;
5 6
6 import javax.validation.constraints.NotNull; 7 import javax.validation.constraints.NotNull;
7 -import java.util.Map; 8 +import java.util.List;
8 9
9 /** 10 /**
10 * 短信发送消息 11 * 短信发送消息
@@ -15,28 +16,29 @@ import java.util.Map; @@ -15,28 +16,29 @@ import java.util.Map;
15 public class SysSmsSendMessage implements StreamMessage { 16 public class SysSmsSendMessage implements StreamMessage {
16 17
17 /** 18 /**
  19 + * 短信日志编号
  20 + */
  21 + @NotNull(message = "短信日志编号不能为空")
  22 + private Long logId;
  23 + /**
18 * 手机号 24 * 手机号
19 */ 25 */
20 @NotNull(message = "手机号不能为空") 26 @NotNull(message = "手机号不能为空")
21 private String mobile; 27 private String mobile;
22 /** 28 /**
23 - * 短信模板编号 29 + * 短信渠道编号
24 */ 30 */
25 - @NotNull(message = "短信模板编号不能为空")  
26 - private String templateCode; 31 + @NotNull(message = "短信渠道编号不能为空")
  32 + private Long channelId;
27 /** 33 /**
28 - * 短信模板参数 34 + * 短信 API 的模板编号
29 */ 35 */
30 - private Map<String, Object> templateParams;  
31 - 36 + @NotNull(message = "短信 API 的模板编号不能为空")
  37 + private String apiTemplateId;
32 /** 38 /**
33 - * 用户编号,允许空  
34 - */  
35 - private Integer userId;  
36 - /**  
37 - * 用户类型,允许空 39 + * 短信模板参数
38 */ 40 */
39 - private Integer userType; 41 + private List<KeyValue<String, Object>> templateParams;
40 42
41 @Override 43 @Override
42 public String getStreamKey() { 44 public String getStreamKey() {
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/sms/SysSmsTemplateRefreshMessage.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.message.sms;
  2 +
  3 +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 短信模板的数据刷新 Message
  8 + */
  9 +@Data
  10 +public class SysSmsTemplateRefreshMessage implements ChannelMessage {
  11 +
  12 + @Override
  13 + public String getChannel() {
  14 + return "system.sms-template.refresh";
  15 + }
  16 +
  17 +}
src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java
@@ -9,6 +9,8 @@ import javax.annotation.Resource; @@ -9,6 +9,8 @@ import javax.annotation.Resource;
9 9
10 /** 10 /**
11 * Role 角色相关消息的 Producer 11 * Role 角色相关消息的 Producer
  12 + *
  13 + * @author 芋道源码
12 */ 14 */
13 @Component 15 @Component
14 public class SysRoleProducer { 16 public class SysRoleProducer {
src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/sms/SysSmsProducer.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.mq.producer.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.core.KeyValue;
  4 +import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils;
  5 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsChannelRefreshMessage;
  6 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
  7 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsTemplateRefreshMessage;
  8 +import lombok.extern.slf4j.Slf4j;
  9 +import org.springframework.data.redis.core.StringRedisTemplate;
  10 +import org.springframework.stereotype.Component;
  11 +
  12 +import javax.annotation.Resource;
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * Sms 短信相关消息的 Producer
  17 + *
  18 + * @author zzf
  19 + * @date 2021/3/9 16:35
  20 + */
  21 +@Slf4j
  22 +@Component
  23 +public class SysSmsProducer {
  24 +
  25 + @Resource
  26 + private StringRedisTemplate stringRedisTemplate;
  27 +
  28 + /**
  29 + * 发送 {@link SysSmsSendMessage} 消息
  30 + *
  31 + * @param logId 短信日志编号
  32 + * @param mobile 手机号
  33 + * @param channelId 渠道编号
  34 + * @param apiTemplateId 短信模板编号
  35 + * @param templateParams 短信模板参数
  36 + */
  37 + public void sendSmsSendMessage(Long logId, String mobile,
  38 + Long channelId, String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
  39 + SysSmsSendMessage message = new SysSmsSendMessage().setLogId(logId).setMobile(mobile);
  40 + message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams);
  41 + RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message);
  42 + }
  43 +
  44 + /**
  45 + * 发送 {@link SysSmsChannelRefreshMessage} 消息
  46 + */
  47 + public void sendSmsChannelRefreshMessage() {
  48 + SysSmsChannelRefreshMessage message = new SysSmsChannelRefreshMessage();
  49 + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message);
  50 + }
  51 +
  52 + /**
  53 + * 发送 {@link SysSmsTemplateRefreshMessage} 消息
  54 + */
  55 + public void sendSmsTemplateRefreshMessage() {
  56 + SysSmsTemplateRefreshMessage message = new SysSmsTemplateRefreshMessage();
  57 + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message);
  58 + }
  59 +
  60 +}
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java
@@ -18,7 +18,7 @@ import java.util.Set; @@ -18,7 +18,7 @@ import java.util.Set;
18 public interface SysPermissionService extends SecurityPermissionFrameworkService { 18 public interface SysPermissionService extends SecurityPermissionFrameworkService {
19 19
20 /** 20 /**
21 - * 初始化 21 + * 初始化权限的本地缓存
22 */ 22 */
23 void initLocalCache(); 23 void initLocalCache();
24 24
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java
@@ -15,6 +15,7 @@ import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysMenuProduce @@ -15,6 +15,7 @@ import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysMenuProduce
15 import cn.iocoder.dashboard.modules.system.service.permission.SysMenuService; 15 import cn.iocoder.dashboard.modules.system.service.permission.SysMenuService;
16 import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService; 16 import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
17 import cn.iocoder.dashboard.util.collection.CollectionUtils; 17 import cn.iocoder.dashboard.util.collection.CollectionUtils;
  18 +import com.google.common.annotations.VisibleForTesting;
18 import com.google.common.collect.ImmutableMap; 19 import com.google.common.collect.ImmutableMap;
19 import com.google.common.collect.ImmutableMultimap; 20 import com.google.common.collect.ImmutableMultimap;
20 import com.google.common.collect.Multimap; 21 import com.google.common.collect.Multimap;
@@ -168,10 +169,6 @@ public class SysMenuServiceImpl implements SysMenuService { @@ -168,10 +169,6 @@ public class SysMenuServiceImpl implements SysMenuService {
168 */ 169 */
169 @Transactional(rollbackFor = Exception.class) 170 @Transactional(rollbackFor = Exception.class)
170 public void deleteMenu(Long menuId) { 171 public void deleteMenu(Long menuId) {
171 - // 校验更新的菜单是否存在  
172 - if (menuMapper.selectById(menuId) == null) {  
173 - throw ServiceExceptionUtil.exception(MENU_NOT_EXISTS);  
174 - }  
175 // 校验是否还有子菜单 172 // 校验是否还有子菜单
176 if (menuMapper.selectCountByParentId(menuId) > 0) { 173 if (menuMapper.selectCountByParentId(menuId) > 0) {
177 throw ServiceExceptionUtil.exception(MENU_EXISTS_CHILDREN); 174 throw ServiceExceptionUtil.exception(MENU_EXISTS_CHILDREN);
@@ -250,7 +247,8 @@ public class SysMenuServiceImpl implements SysMenuService { @@ -250,7 +247,8 @@ public class SysMenuServiceImpl implements SysMenuService {
250 * @param parentId 父菜单编号 247 * @param parentId 父菜单编号
251 * @param childId 当前菜单编号 248 * @param childId 当前菜单编号
252 */ 249 */
253 - private void checkParentResource(Long parentId, Long childId) { 250 + @VisibleForTesting
  251 + public void checkParentResource(Long parentId, Long childId) {
254 if (parentId == null || MenuIdEnum.ROOT.getId().equals(parentId)) { 252 if (parentId == null || MenuIdEnum.ROOT.getId().equals(parentId)) {
255 return; 253 return;
256 } 254 }
@@ -279,7 +277,8 @@ public class SysMenuServiceImpl implements SysMenuService { @@ -279,7 +277,8 @@ public class SysMenuServiceImpl implements SysMenuService {
279 * @param parentId 父菜单编号 277 * @param parentId 父菜单编号
280 * @param id 菜单编号 278 * @param id 菜单编号
281 */ 279 */
282 - private void checkResource(Long parentId, String name, Long id) { 280 + @VisibleForTesting
  281 + public void checkResource(Long parentId, String name, Long id) {
283 SysMenuDO menu = menuMapper.selectByParentIdAndName(parentId, name); 282 SysMenuDO menu = menuMapper.selectByParentIdAndName(parentId, name);
284 if (menu == null) { 283 if (menu == null) {
285 return; 284 return;
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java
@@ -3,7 +3,6 @@ package cn.iocoder.dashboard.modules.system.service.permission.impl; @@ -3,7 +3,6 @@ package cn.iocoder.dashboard.modules.system.service.permission.impl;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.collection.CollectionUtil; 4 import cn.hutool.core.collection.CollectionUtil;
5 import cn.hutool.core.util.ArrayUtil; 5 import cn.hutool.core.util.ArrayUtil;
6 -import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;  
7 import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils; 6 import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
8 import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysRoleMenuMapper; 7 import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysRoleMenuMapper;
9 import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysUserRoleMapper; 8 import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysUserRoleMapper;
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java
@@ -18,6 +18,7 @@ import cn.iocoder.dashboard.modules.system.enums.permission.SysRoleTypeEnum; @@ -18,6 +18,7 @@ import cn.iocoder.dashboard.modules.system.enums.permission.SysRoleTypeEnum;
18 import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysRoleProducer; 18 import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysRoleProducer;
19 import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService; 19 import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
20 import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService; 20 import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService;
  21 +import com.google.common.annotations.VisibleForTesting;
21 import com.google.common.collect.ImmutableMap; 22 import com.google.common.collect.ImmutableMap;
22 import lombok.extern.slf4j.Slf4j; 23 import lombok.extern.slf4j.Slf4j;
23 import org.springframework.lang.Nullable; 24 import org.springframework.lang.Nullable;
@@ -58,7 +59,7 @@ public class SysRoleServiceImpl implements SysRoleService { @@ -58,7 +59,7 @@ public class SysRoleServiceImpl implements SysRoleService {
58 */ 59 */
59 private volatile Map<Long, SysRoleDO> roleCache; 60 private volatile Map<Long, SysRoleDO> roleCache;
60 /** 61 /**
61 - * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新 62 + * 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新
62 */ 63 */
63 private volatile Date maxUpdateTime; 64 private volatile Date maxUpdateTime;
64 65
@@ -77,7 +78,7 @@ public class SysRoleServiceImpl implements SysRoleService { @@ -77,7 +78,7 @@ public class SysRoleServiceImpl implements SysRoleService {
77 @Override 78 @Override
78 @PostConstruct 79 @PostConstruct
79 public void initLocalCache() { 80 public void initLocalCache() {
80 - // 获取菜单列表,如果有更新 81 + // 获取角色列表,如果有更新
81 List<SysRoleDO> roleList = this.loadRoleIfUpdate(maxUpdateTime); 82 List<SysRoleDO> roleList = this.loadRoleIfUpdate(maxUpdateTime);
82 if (CollUtil.isEmpty(roleList)) { 83 if (CollUtil.isEmpty(roleList)) {
83 return; 84 return;
@@ -98,23 +99,23 @@ public class SysRoleServiceImpl implements SysRoleService { @@ -98,23 +99,23 @@ public class SysRoleServiceImpl implements SysRoleService {
98 } 99 }
99 100
100 /** 101 /**
101 - * 如果菜单发生变化,从数据库中获取最新的全量菜单。 102 + * 如果角色发生变化,从数据库中获取最新的全量角色。
102 * 如果未发生变化,则返回空 103 * 如果未发生变化,则返回空
103 * 104 *
104 - * @param maxUpdateTime 当前菜单的最大更新时间  
105 - * @return 菜单列表 105 + * @param maxUpdateTime 当前角色的最大更新时间
  106 + * @return 角色列表
106 */ 107 */
107 private List<SysRoleDO> loadRoleIfUpdate(Date maxUpdateTime) { 108 private List<SysRoleDO> loadRoleIfUpdate(Date maxUpdateTime) {
108 // 第一步,判断是否要更新。 109 // 第一步,判断是否要更新。
109 if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 110 if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
110 - log.info("[loadRoleIfUpdate][首次加载全量菜单]");  
111 - } else { // 判断数据库中是否有更新的菜单 111 + log.info("[loadRoleIfUpdate][首次加载全量角色]");
  112 + } else { // 判断数据库中是否有更新的角色
112 if (!roleMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) { 113 if (!roleMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) {
113 return null; 114 return null;
114 } 115 }
115 - log.info("[loadRoleIfUpdate][增量加载全量菜单]"); 116 + log.info("[loadRoleIfUpdate][增量加载全量角色]");
116 } 117 }
117 - // 第二步,如果有更新,则从数据库加载所有菜单 118 + // 第二步,如果有更新,则从数据库加载所有角色
118 return roleMapper.selectList(); 119 return roleMapper.selectList();
119 } 120 }
120 121
@@ -245,7 +246,8 @@ public class SysRoleServiceImpl implements SysRoleService { @@ -245,7 +246,8 @@ public class SysRoleServiceImpl implements SysRoleService {
245 * @param code 角色额编码 246 * @param code 角色额编码
246 * @param id 角色编号 247 * @param id 角色编号
247 */ 248 */
248 - private void checkDuplicateRole(String name, String code, Long id) { 249 + @VisibleForTesting
  250 + public void checkDuplicateRole(String name, String code, Long id) {
249 // 1. 该 name 名字被其它角色所使用 251 // 1. 该 name 名字被其它角色所使用
250 SysRoleDO role = roleMapper.selectByName(name); 252 SysRoleDO role = roleMapper.selectByName(name);
251 if (role != null && !role.getId().equals(id)) { 253 if (role != null && !role.getId().equals(id)) {
@@ -258,7 +260,7 @@ public class SysRoleServiceImpl implements SysRoleService { @@ -258,7 +260,7 @@ public class SysRoleServiceImpl implements SysRoleService {
258 // 该 code 编码被其它角色所使用 260 // 该 code 编码被其它角色所使用
259 role = roleMapper.selectByCode(code); 261 role = roleMapper.selectByCode(code);
260 if (role != null && !role.getId().equals(id)) { 262 if (role != null && !role.getId().equals(id)) {
261 - throw ServiceExceptionUtil.exception(ROLE_CODE_DUPLICATE, name); 263 + throw ServiceExceptionUtil.exception(ROLE_CODE_DUPLICATE, code);
262 } 264 }
263 } 265 }
264 266
@@ -267,7 +269,8 @@ public class SysRoleServiceImpl implements SysRoleService { @@ -267,7 +269,8 @@ public class SysRoleServiceImpl implements SysRoleService {
267 * 269 *
268 * @param id 角色编号 270 * @param id 角色编号
269 */ 271 */
270 - private void checkUpdateRole(Long id) { 272 + @VisibleForTesting
  273 + public void checkUpdateRole(Long id) {
271 SysRoleDO roleDO = roleMapper.selectById(id); 274 SysRoleDO roleDO = roleMapper.selectById(id);
272 if (roleDO == null) { 275 if (roleDO == null) {
273 throw ServiceExceptionUtil.exception(ROLE_NOT_EXISTS); 276 throw ServiceExceptionUtil.exception(ROLE_NOT_EXISTS);
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsChannelService.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageResult;
  4 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO;
  5 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO;
  6 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO;
  7 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
  8 +
  9 +import javax.validation.Valid;
  10 +import java.util.Collection;
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * 短信渠道Service接口
  15 + *
  16 + * @author zzf
  17 + * @date 2021/1/25 9:24
  18 + */
  19 +public interface SysSmsChannelService {
  20 +
  21 + /**
  22 + * 初始化短信客户端
  23 + */
  24 + void initSmsClients();
  25 +
  26 + /**
  27 + * 创建短信渠道
  28 + *
  29 + * @param createReqVO 创建信息
  30 + * @return 编号
  31 + */
  32 + Long createSmsChannel(@Valid SysSmsChannelCreateReqVO createReqVO);
  33 +
  34 + /**
  35 + * 更新短信渠道
  36 + *
  37 + * @param updateReqVO 更新信息
  38 + */
  39 + void updateSmsChannel(@Valid SysSmsChannelUpdateReqVO updateReqVO);
  40 +
  41 + /**
  42 + * 删除短信渠道
  43 + *
  44 + * @param id 编号
  45 + */
  46 + void deleteSmsChannel(Long id);
  47 +
  48 + /**
  49 + * 获得短信渠道
  50 + *
  51 + * @param id 编号
  52 + * @return 短信渠道
  53 + */
  54 + SysSmsChannelDO getSmsChannel(Long id);
  55 +
  56 + /**
  57 + * 获得短信渠道列表
  58 + *
  59 + * @param ids 编号
  60 + * @return 短信渠道列表
  61 + */
  62 + List<SysSmsChannelDO> getSmsChannelList(Collection<Long> ids);
  63 +
  64 + /**
  65 + * 获得所有短信渠道列表
  66 + *
  67 + * @return 短信渠道列表
  68 + */
  69 + List<SysSmsChannelDO> getSmsChannelList();
  70 +
  71 + /**
  72 + * 获得短信渠道分页
  73 + *
  74 + * @param pageReqVO 分页查询
  75 + * @return 短信渠道分页
  76 + */
  77 + PageResult<SysSmsChannelDO> getSmsChannelPage(SysSmsChannelPageReqVO pageReqVO);
  78 +
  79 +}
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsLogService.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageResult;
  4 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO;
  5 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO;
  6 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
  7 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  8 +
  9 +import java.util.Date;
  10 +import java.util.List;
  11 +import java.util.Map;
  12 +
  13 +/**
  14 + * 短信日志 Service 实现类
  15 + *
  16 + * @author zzf
  17 + * @date 13:48 2021/3/2
  18 + */
  19 +public interface SysSmsLogService {
  20 +
  21 + /**
  22 + * 创建短信日志
  23 + *
  24 + * @param mobile 手机号
  25 + * @param userId 用户编号
  26 + * @param userType 用户类型
  27 + * @param isSend 是否发送
  28 + * @param template 短信模板
  29 + * @param templateContent 短信内容
  30 + * @param templateParams 短信参数
  31 + * @return 发送日志编号
  32 + */
  33 + Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend,
  34 + SysSmsTemplateDO template, String templateContent, Map<String, Object> templateParams);
  35 +
  36 + /**
  37 + * 更新日志的发送结果
  38 + *
  39 + * @param id 日志编号
  40 + * @param sendCode 发送结果的编码
  41 + * @param sendMsg 发送结果的提示
  42 + * @param apiSendCode 短信 API 发送结果的编码
  43 + * @param apiSendMsg 短信 API 发送失败的提示
  44 + * @param apiRequestId 短信 API 发送返回的唯一请求 ID
  45 + * @param apiSerialNo 短信 API 发送返回的序号
  46 + */
  47 + void updateSmsSendResult(Long id, Integer sendCode, String sendMsg,
  48 + String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo);
  49 +
  50 + /**
  51 + * 更新日志的接收结果
  52 + *
  53 + * @param id 日志编号
  54 + * @param success 是否接收成功
  55 + * @param receiveTime 用户接收时间
  56 + * @param apiReceiveCode API 接收结果的编码
  57 + * @param apiReceiveMsg API 接收结果的说明
  58 + */
  59 + void updateSmsReceiveResult(Long id, Boolean success, Date receiveTime, String apiReceiveCode, String apiReceiveMsg);
  60 +
  61 + /**
  62 + * 获得短信日志分页
  63 + *
  64 + * @param pageReqVO 分页查询
  65 + * @return 短信日志分页
  66 + */
  67 + PageResult<SysSmsLogDO> getSmsLogPage(SysSmsLogPageReqVO pageReqVO);
  68 +
  69 + /**
  70 + * 获得短信日志列表, 用于 Excel 导出
  71 + *
  72 + * @param exportReqVO 查询条件
  73 + * @return 短信日志列表
  74 + */
  75 + List<SysSmsLogDO> getSmsLogList(SysSmsLogExportReqVO exportReqVO);
  76 +
  77 +}
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsService.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms;
  2 +
  3 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
  4 +
  5 +import java.util.List;
  6 +import java.util.Map;
  7 +
  8 +/**
  9 + * 短信Service接口
  10 + * 只支持异步,因此没有返回值
  11 + *
  12 + * @author zzf
  13 + * @date 2021/1/25 9:24
  14 + */
  15 +public interface SysSmsService {
  16 +
  17 + /**
  18 + * 发送单条短信给用户(管理员)
  19 + *
  20 + * 在 mobile 为空时,使用 userId 加载对应管理员的手机号
  21 + *
  22 + * @param mobile 手机号
  23 + * @param userId 用户编号
  24 + * @param templateCode 短信模板编号
  25 + * @param templateParams 短信模板参数
  26 + * @return 发送日志编号
  27 + */
  28 + Long sendSingleSmsToAdmin(String mobile, Long userId,
  29 + String templateCode, Map<String, Object> templateParams);
  30 +
  31 + /**
  32 + * 发送单条短信给用户(会员)
  33 + *
  34 + * 在 mobile 为空时,使用 userId 加载对应会员的手机号
  35 + *
  36 + * @param mobile 手机号
  37 + * @param userId 用户编号
  38 + * @param templateCode 短信模板编号
  39 + * @param templateParams 短信模板参数
  40 + * @return 发送日志编号
  41 + */
  42 + Long sendSingleSmsToMember(String mobile, Long userId,
  43 + String templateCode, Map<String, Object> templateParams);
  44 +
  45 + Long sendSingleSms(String mobile, Long userId, Integer userType,
  46 + String templateCode, Map<String, Object> templateParams);
  47 +
  48 + void sendBatchSms(List<String> mobiles, List<Long> userIds, Integer userType,
  49 + String templateCode, Map<String, Object> templateParams);
  50 +
  51 + void doSendSms(SysSmsSendMessage message);
  52 +
  53 + /**
  54 + * 接收短信的接收结果
  55 + *
  56 + * @param channelCode 渠道编码
  57 + * @param text 结果内容
  58 + * @throws Throwable 处理失败时,抛出异常
  59 + */
  60 + void receiveSmsStatus(String channelCode, String text) throws Throwable;
  61 +
  62 +}
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsTemplateService.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.PageResult;
  4 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO;
  5 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO;
  6 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO;
  7 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO;
  8 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  9 +
  10 +import javax.validation.Valid;
  11 +import java.util.Collection;
  12 +import java.util.List;
  13 +import java.util.Map;
  14 +
  15 +/**
  16 + * 短信模板 Service 接口
  17 + *
  18 + * @author zzf
  19 + * @date 2021/1/25 9:24
  20 + */
  21 +public interface SysSmsTemplateService {
  22 +
  23 + /**
  24 + * 初始化短信模板的本地缓存
  25 + */
  26 + void initLocalCache();
  27 +
  28 + /**
  29 + * 获得短信模板
  30 + *
  31 + * @param code 模板编码
  32 + * @return 短信模板
  33 + */
  34 + SysSmsTemplateDO getSmsTemplateByCode(String code);
  35 +
  36 + /**
  37 + * 获得短信模板,从缓存中
  38 + *
  39 + * @param code 模板编码
  40 + * @return 短信模板
  41 + */
  42 + SysSmsTemplateDO getSmsTemplateByCodeFromCache(String code);
  43 +
  44 + /**
  45 + * 格式化短信内容
  46 + *
  47 + * @param content 短信模板的内容
  48 + * @param params 内容的参数
  49 + * @return 格式化后的内容
  50 + */
  51 + String formatSmsTemplateContent(String content, Map<String, Object> params);
  52 +
  53 + /**
  54 + * 创建短信模板
  55 + *
  56 + * @param createReqVO 创建信息
  57 + * @return 编号
  58 + */
  59 + Long createSmsTemplate(@Valid SysSmsTemplateCreateReqVO createReqVO);
  60 +
  61 + /**
  62 + * 更新短信模板
  63 + *
  64 + * @param updateReqVO 更新信息
  65 + */
  66 + void updateSmsTemplate(@Valid SysSmsTemplateUpdateReqVO updateReqVO);
  67 +
  68 + /**
  69 + * 删除短信模板
  70 + *
  71 + * @param id 编号
  72 + */
  73 + void deleteSmsTemplate(Long id);
  74 +
  75 + /**
  76 + * 获得短信模板
  77 + *
  78 + * @param id 编号
  79 + * @return 短信模板
  80 + */
  81 + SysSmsTemplateDO getSmsTemplate(Long id);
  82 +
  83 + /**
  84 + * 获得短信模板列表
  85 + *
  86 + * @param ids 编号
  87 + * @return 短信模板列表
  88 + */
  89 + List<SysSmsTemplateDO> getSmsTemplateList(Collection<Long> ids);
  90 +
  91 + /**
  92 + * 获得短信模板分页
  93 + *
  94 + * @param pageReqVO 分页查询
  95 + * @return 短信模板分页
  96 + */
  97 + PageResult<SysSmsTemplateDO> getSmsTemplatePage(SysSmsTemplatePageReqVO pageReqVO);
  98 +
  99 + /**
  100 + * 获得短信模板列表, 用于 Excel 导出
  101 + *
  102 + * @param exportReqVO 查询条件
  103 + * @return 短信模板分页
  104 + */
  105 + List<SysSmsTemplateDO> getSmsTemplateList(SysSmsTemplateExportReqVO exportReqVO);
  106 +
  107 + /**
  108 + * 获得指定短信渠道下的短信模板数量
  109 + *
  110 + * @param channelId 短信渠道编号
  111 + * @return 数量
  112 + */
  113 + Integer countByChannelId(Long channelId);
  114 +
  115 +}
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsChannelServiceImpl.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms.impl;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.dashboard.common.pojo.PageResult;
  5 +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
  6 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
  7 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  8 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO;
  9 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO;
  10 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO;
  11 +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsChannelConvert;
  12 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
  13 +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsChannelMapper;
  14 +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
  15 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
  16 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.springframework.scheduling.annotation.Scheduled;
  19 +import org.springframework.stereotype.Service;
  20 +
  21 +import javax.annotation.PostConstruct;
  22 +import javax.annotation.Resource;
  23 +import java.util.Collection;
  24 +import java.util.Comparator;
  25 +import java.util.Date;
  26 +import java.util.List;
  27 +
  28 +import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
  29 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
  30 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
  31 +
  32 +/**
  33 + * 短信渠道Service实现类
  34 + *
  35 + * @author zzf
  36 + * @date 2021/1/25 9:25
  37 + */
  38 +@Service
  39 +@Slf4j
  40 +public class SysSmsChannelServiceImpl implements SysSmsChannelService {
  41 +
  42 + /**
  43 + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期
  44 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
  45 + */
  46 + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
  47 +
  48 + /**
  49 + * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
  50 + */
  51 + private volatile Date maxUpdateTime;
  52 +
  53 + @Resource
  54 + private SmsClientFactory smsClientFactory;
  55 +
  56 + @Resource
  57 + private SysSmsChannelMapper smsChannelMapper;
  58 +
  59 + @Resource
  60 + private SysSmsTemplateService smsTemplateService;
  61 +
  62 + @Resource
  63 + private SysSmsProducer smsProducer;
  64 +
  65 + @Override
  66 + @PostConstruct
  67 + public void initSmsClients() {
  68 + // 获取短信渠道,如果有更新
  69 + List<SysSmsChannelDO> smsChannels = this.loadSmsChannelIfUpdate(maxUpdateTime);
  70 + if (CollUtil.isEmpty(smsChannels)) {
  71 + return;
  72 + }
  73 +
  74 + // 创建或更新短信 Client
  75 + List<SmsChannelProperties> propertiesList = SysSmsChannelConvert.INSTANCE.convertList02(smsChannels);
  76 + propertiesList.forEach(properties -> smsClientFactory.createOrUpdateSmsClient(properties));
  77 +
  78 + // 写入缓存
  79 + assert smsChannels.size() > 0; // 断言,避免告警
  80 + maxUpdateTime = smsChannels.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
  81 + log.info("[initSmsClients][初始化 SmsChannel 数量为 {}]", smsChannels.size());
  82 + }
  83 +
  84 + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
  85 + public void schedulePeriodicRefresh() {
  86 + initSmsClients();
  87 + }
  88 +
  89 + /**
  90 + * 如果短信渠道发生变化,从数据库中获取最新的全量短信渠道。
  91 + * 如果未发生变化,则返回空
  92 + *
  93 + * @param maxUpdateTime 当前短信渠道的最大更新时间
  94 + * @return 短信渠道列表
  95 + */
  96 + private List<SysSmsChannelDO> loadSmsChannelIfUpdate(Date maxUpdateTime) {
  97 + // 第一步,判断是否要更新。
  98 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
  99 + log.info("[loadSmsChannelIfUpdate][首次加载全量短信渠道]");
  100 + } else { // 判断数据库中是否有更新的短信渠道
  101 + if (smsChannelMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
  102 + return null;
  103 + }
  104 + log.info("[loadSmsChannelIfUpdate][增量加载全量短信渠道]");
  105 + }
  106 + // 第二步,如果有更新,则从数据库加载所有短信渠道
  107 + return smsChannelMapper.selectList();
  108 + }
  109 +
  110 + @Override
  111 + public Long createSmsChannel(SysSmsChannelCreateReqVO createReqVO) {
  112 + // 插入
  113 + SysSmsChannelDO smsChannel = SysSmsChannelConvert.INSTANCE.convert(createReqVO);
  114 + smsChannelMapper.insert(smsChannel);
  115 + // 发送刷新消息
  116 + smsProducer.sendSmsChannelRefreshMessage();
  117 + // 返回
  118 + return smsChannel.getId();
  119 + }
  120 +
  121 + @Override
  122 + public void updateSmsChannel(SysSmsChannelUpdateReqVO updateReqVO) {
  123 + // 校验存在
  124 + this.validateSmsChannelExists(updateReqVO.getId());
  125 + // 更新
  126 + SysSmsChannelDO updateObj = SysSmsChannelConvert.INSTANCE.convert(updateReqVO);
  127 + smsChannelMapper.updateById(updateObj);
  128 + // 发送刷新消息
  129 + smsProducer.sendSmsChannelRefreshMessage();
  130 + }
  131 +
  132 + @Override
  133 + public void deleteSmsChannel(Long id) {
  134 + // 校验存在
  135 + this.validateSmsChannelExists(id);
  136 + // 校验是否有字典数据
  137 + if (smsTemplateService.countByChannelId(id) > 0) {
  138 + throw exception(SMS_CHANNEL_HAS_CHILDREN);
  139 + }
  140 + // 删除
  141 + smsChannelMapper.deleteById(id);
  142 + // 发送刷新消息
  143 + smsProducer.sendSmsChannelRefreshMessage();
  144 + }
  145 +
  146 + private void validateSmsChannelExists(Long id) {
  147 + if (smsChannelMapper.selectById(id) == null) {
  148 + throw exception(SMS_CHANNEL_NOT_EXISTS);
  149 + }
  150 + }
  151 +
  152 + @Override
  153 + public SysSmsChannelDO getSmsChannel(Long id) {
  154 + return smsChannelMapper.selectById(id);
  155 + }
  156 +
  157 + @Override
  158 + public List<SysSmsChannelDO> getSmsChannelList(Collection<Long> ids) {
  159 + return smsChannelMapper.selectBatchIds(ids);
  160 + }
  161 +
  162 + @Override
  163 + public List<SysSmsChannelDO> getSmsChannelList() {
  164 + return smsChannelMapper.selectList();
  165 + }
  166 +
  167 + @Override
  168 + public PageResult<SysSmsChannelDO> getSmsChannelPage(SysSmsChannelPageReqVO pageReqVO) {
  169 + return smsChannelMapper.selectPage(pageReqVO);
  170 + }
  171 +
  172 +}
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsLogServiceImpl.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms.impl;
  2 +
  3 +import cn.iocoder.dashboard.common.pojo.CommonResult;
  4 +import cn.iocoder.dashboard.common.pojo.PageResult;
  5 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO;
  6 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO;
  7 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
  8 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  9 +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsLogMapper;
  10 +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsReceiveStatusEnum;
  11 +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum;
  12 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.stereotype.Service;
  15 +
  16 +import javax.annotation.Resource;
  17 +import java.util.Date;
  18 +import java.util.List;
  19 +import java.util.Map;
  20 +import java.util.Objects;
  21 +
  22 +/**
  23 + * 短信日志 Service 实现类
  24 + *
  25 + * @author zzf
  26 + * @date 2021/1/25 9:25
  27 + */
  28 +@Slf4j
  29 +@Service
  30 +public class SysSmsLogServiceImpl implements SysSmsLogService {
  31 +
  32 + @Resource
  33 + private SysSmsLogMapper smsLogMapper;
  34 +
  35 + @Override
  36 + public Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend,
  37 + SysSmsTemplateDO template, String templateContent, Map<String, Object> templateParams) {
  38 + SysSmsLogDO.SysSmsLogDOBuilder logBuilder = SysSmsLogDO.builder();
  39 + // 根据是否要发送,设置状态
  40 + logBuilder.sendStatus(Objects.equals(isSend, true) ? SysSmsSendStatusEnum.INIT.getStatus()
  41 + : SysSmsSendStatusEnum.IGNORE.getStatus());
  42 + // 设置手机相关字段
  43 + logBuilder.mobile(mobile).userId(userId).userType(userType);
  44 + // 设置模板相关字段
  45 + logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType());
  46 + logBuilder.templateContent(templateContent).templateParams(templateParams).apiTemplateId(template.getApiTemplateId());
  47 + // 设置渠道相关字段
  48 + logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode());
  49 + // 设置接收相关字段
  50 + logBuilder.receiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus());
  51 +
  52 + // 插入数据库
  53 + SysSmsLogDO logDO = logBuilder.build();
  54 + smsLogMapper.insert(logDO);
  55 + return logDO.getId();
  56 + }
  57 +
  58 + @Override
  59 + public void updateSmsSendResult(Long id, Integer sendCode, String sendMsg,
  60 + String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo) {
  61 + SysSmsSendStatusEnum sendStatus = CommonResult.isSuccess(sendCode) ? SysSmsSendStatusEnum.SUCCESS
  62 + : SysSmsSendStatusEnum.FAILURE;
  63 + smsLogMapper.updateById(SysSmsLogDO.builder().id(id).sendStatus(sendStatus.getStatus()).sendTime(new Date())
  64 + .sendCode(sendCode).sendMsg(sendMsg).apiSendCode(apiSendCode).apiSendMsg(apiSendMsg)
  65 + .apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).build());
  66 + }
  67 +
  68 + @Override
  69 + public void updateSmsReceiveResult(Long id, Boolean success, Date receiveTime, String apiReceiveCode, String apiReceiveMsg) {
  70 + SysSmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? SysSmsReceiveStatusEnum.SUCCESS
  71 + : SysSmsReceiveStatusEnum.FAILURE;
  72 + smsLogMapper.updateById(SysSmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()).receiveTime(receiveTime)
  73 + .apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build());
  74 + }
  75 +
  76 + @Override
  77 + public PageResult<SysSmsLogDO> getSmsLogPage(SysSmsLogPageReqVO pageReqVO) {
  78 + return smsLogMapper.selectPage(pageReqVO);
  79 + }
  80 +
  81 + @Override
  82 + public List<SysSmsLogDO> getSmsLogList(SysSmsLogExportReqVO exportReqVO) {
  83 + return smsLogMapper.selectList(exportReqVO);
  84 + }
  85 +
  86 +}
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsServiceImpl.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms.impl;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.dashboard.common.core.KeyValue;
  6 +import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.dashboard.common.enums.UserTypeEnum;
  8 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
  9 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
  10 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  11 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
  12 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  13 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  14 +import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
  15 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
  16 +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
  17 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsLogService;
  18 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsService;
  19 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
  20 +import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
  21 +import com.google.common.annotations.VisibleForTesting;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +import org.springframework.stereotype.Service;
  24 +import org.springframework.util.Assert;
  25 +
  26 +import javax.annotation.Resource;
  27 +import java.util.List;
  28 +import java.util.Map;
  29 +import java.util.stream.Collectors;
  30 +
  31 +import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
  32 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
  33 +
  34 +/**
  35 + * 短信日志Service实现类
  36 + *
  37 + * @author zzf
  38 + * @date 2021/1/25 9:25
  39 + */
  40 +@Service
  41 +@Slf4j
  42 +public class SysSmsServiceImpl implements SysSmsService {
  43 +
  44 + @Resource
  45 + private SysSmsTemplateService smsTemplateService;
  46 + @Resource
  47 + private SysSmsLogService smsLogService;
  48 + @Resource
  49 + private SysSmsProducer smsProducer;
  50 + @Resource
  51 + private SmsClientFactory smsClientFactory;
  52 +
  53 + @Resource
  54 + private SysUserService userService;
  55 +
  56 + @Override
  57 + public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
  58 + // 如果 mobile 为空,则加载用户编号对应的手机号
  59 + if (StrUtil.isEmpty(mobile)) {
  60 + SysUserDO user = userService.getUser(userId);
  61 + if (user != null) {
  62 + mobile = user.getMobile();
  63 + }
  64 + }
  65 + // 执行发送
  66 + return this.sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams);
  67 + }
  68 +
  69 + @Override
  70 + public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
  71 + throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!");
  72 + }
  73 +
  74 + @Override
  75 + public Long sendSingleSms(String mobile, Long userId, Integer userType,
  76 + String templateCode, Map<String, Object> templateParams) {
  77 + // 校验短信模板是否合法
  78 + SysSmsTemplateDO template = this.checkSmsTemplateValid(templateCode);
  79 + // 校验手机号码是否存在
  80 + mobile = this.checkMobile(mobile);
  81 + // 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
  82 + List<KeyValue<String, Object>> newTemplateParams = this.buildTemplateParams(template, templateParams);
  83 +
  84 + // 创建发送日志
  85 + Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); // 如果模板被禁用,则不发送短信,只记录日志
  86 + String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
  87 + Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams);
  88 +
  89 + // 发送 MQ 消息,异步执行发送短信
  90 + if (isSend) {
  91 + smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(), template.getApiTemplateId(), newTemplateParams);
  92 + }
  93 + return sendLogId;
  94 + }
  95 +
  96 + @Override
  97 + public void sendBatchSms(List<String> mobiles, List<Long> userIds, Integer userType,
  98 + String templateCode, Map<String, Object> templateParams) {
  99 + throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!");
  100 + }
  101 +
  102 + @VisibleForTesting
  103 + public SysSmsTemplateDO checkSmsTemplateValid(String templateCode) {
  104 + // 获得短信模板。考虑到效率,从缓存中获取
  105 + SysSmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode);
  106 + // 短信模板不存在
  107 + if (template == null) {
  108 + throw exception(SMS_TEMPLATE_NOT_EXISTS);
  109 + }
  110 + return template;
  111 + }
  112 +
  113 + /**
  114 + * 将参数模板,处理成有序的 KeyValue 数组
  115 + *
  116 + * 原因是,部分短信平台并不是使用 key 作为参数,而是数组下标,例如说腾讯云 https://cloud.tencent.com/document/product/382/39023
  117 + *
  118 + * @param template 短信模板
  119 + * @param templateParams 原始参数
  120 + * @return 处理后的参数
  121 + */
  122 + @VisibleForTesting
  123 + public List<KeyValue<String, Object>> buildTemplateParams(SysSmsTemplateDO template, Map<String, Object> templateParams) {
  124 + return template.getParams().stream().map(key -> {
  125 + Object value = templateParams.get(key);
  126 + if (value == null) {
  127 + throw exception(SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, key);
  128 + }
  129 + return new KeyValue<>(key, value);
  130 + }).collect(Collectors.toList());
  131 + }
  132 +
  133 + @VisibleForTesting
  134 + public String checkMobile(String mobile) {
  135 + if (StrUtil.isEmpty(mobile)) {
  136 + throw exception(SMS_SEND_MOBILE_NOT_EXISTS);
  137 + }
  138 + return mobile;
  139 + }
  140 +
  141 + @Override
  142 + public void doSendSms(SysSmsSendMessage message) {
  143 + // 获得渠道对应的 SmsClient 客户端
  144 + SmsClient smsClient = smsClientFactory.getSmsClient(message.getChannelId());
  145 + Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", message.getChannelId()));
  146 + // 发送短信
  147 + SmsCommonResult<SmsSendRespDTO> sendResult = smsClient.sendSms(message.getLogId(), message.getMobile(),
  148 + message.getApiTemplateId(), message.getTemplateParams());
  149 + smsLogService.updateSmsSendResult(message.getLogId(), sendResult.getCode(), sendResult.getMsg(),
  150 + sendResult.getApiCode(), sendResult.getApiMsg(), sendResult.getApiRequestId(),
  151 + sendResult.getData() != null ? sendResult.getData().getSerialNo() : null);
  152 + }
  153 +
  154 + @Override
  155 + public void receiveSmsStatus(String channelCode, String text) throws Throwable {
  156 + // 获得渠道对应的 SmsClient 客户端
  157 + SmsClient smsClient = smsClientFactory.getSmsClient(channelCode);
  158 + Assert.notNull(smsClient, String.format("短信客户端(%s) 不存在", channelCode));
  159 + // 解析内容
  160 + List<SmsReceiveRespDTO> receiveResults = smsClient.parseSmsReceiveStatus(text);
  161 + if (CollUtil.isEmpty(receiveResults)) {
  162 + return;
  163 + }
  164 + // 更新短信日志的接收结果. 因为量一般不打,所以先使用 for 循环更新
  165 + receiveResults.forEach(result -> {
  166 + smsLogService.updateSmsReceiveResult(result.getLogId(), result.getSuccess(), result.getReceiveTime(),
  167 + result.getErrorCode(), result.getErrorCode());
  168 + });
  169 + }
  170 +
  171 +}
src/main/java/cn/iocoder/dashboard/modules/system/service/sms/impl/SysSmsTemplateServiceImpl.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms.impl;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ReUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.dashboard.common.pojo.PageResult;
  8 +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
  9 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
  10 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
  11 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  12 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  13 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO;
  14 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO;
  15 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO;
  16 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO;
  17 +import cn.iocoder.dashboard.modules.system.convert.sms.SysSmsTemplateConvert;
  18 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
  19 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  20 +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsTemplateMapper;
  21 +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
  22 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsChannelService;
  23 +import cn.iocoder.dashboard.modules.system.service.sms.SysSmsTemplateService;
  24 +import com.google.common.annotations.VisibleForTesting;
  25 +import com.google.common.collect.ImmutableMap;
  26 +import lombok.extern.slf4j.Slf4j;
  27 +import org.springframework.scheduling.annotation.Scheduled;
  28 +import org.springframework.stereotype.Service;
  29 +import org.springframework.util.Assert;
  30 +
  31 +import javax.annotation.PostConstruct;
  32 +import javax.annotation.Resource;
  33 +import java.util.*;
  34 +import java.util.regex.Pattern;
  35 +
  36 +import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
  37 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
  38 +
  39 +/**
  40 + * 短信模板Service实现类
  41 + *
  42 + * @author zzf
  43 + * @date 2021/1/25 9:25
  44 + */
  45 +@Service
  46 +@Slf4j
  47 +public class SysSmsTemplateServiceImpl implements SysSmsTemplateService {
  48 +
  49 + /**
  50 + * 正则表达式,匹配 {} 中的变量
  51 + */
  52 + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}");
  53 +
  54 + /**
  55 + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期
  56 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
  57 + */
  58 + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
  59 +
  60 + /**
  61 + * 短信模板缓存
  62 + * key:短信模板编码 {@link SysSmsTemplateDO#getCode()}
  63 + *
  64 + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
  65 + */
  66 + private volatile Map<String, SysSmsTemplateDO> smsTemplateCache;
  67 + /**
  68 + * 缓存短信模板的最大更新时间,用于后续的增量轮询,判断是否有更新
  69 + */
  70 + private volatile Date maxUpdateTime;
  71 +
  72 + @Resource
  73 + private SysSmsTemplateMapper smsTemplateMapper;
  74 +
  75 + @Resource
  76 + private SysSmsChannelService smsChannelService;
  77 +
  78 + @Resource
  79 + private SmsClientFactory smsClientFactory;
  80 +
  81 + @Resource
  82 + private SysSmsProducer smsProducer;
  83 +
  84 + /**
  85 + * 初始化 {@link #smsTemplateCache} 缓存
  86 + */
  87 + @Override
  88 + @PostConstruct
  89 + public void initLocalCache() {
  90 + // 获取短信模板列表,如果有更新
  91 + List<SysSmsTemplateDO> smsTemplateList = this.loadSmsTemplateIfUpdate(maxUpdateTime);
  92 + if (CollUtil.isEmpty(smsTemplateList)) {
  93 + return;
  94 + }
  95 +
  96 + // 写入缓存
  97 + ImmutableMap.Builder<String, SysSmsTemplateDO> builder = ImmutableMap.builder();
  98 + smsTemplateList.forEach(sysSmsTemplateDO -> builder.put(sysSmsTemplateDO.getCode(), sysSmsTemplateDO));
  99 + smsTemplateCache = builder.build();
  100 + assert smsTemplateList.size() > 0; // 断言,避免告警
  101 + maxUpdateTime = smsTemplateList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
  102 + log.info("[initLocalCache][初始化 SmsTemplate 数量为 {}]", smsTemplateList.size());
  103 + }
  104 +
  105 + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
  106 + public void schedulePeriodicRefresh() {
  107 + initLocalCache();
  108 + }
  109 +
  110 + /**
  111 + * 如果短信模板发生变化,从数据库中获取最新的全量短信模板。
  112 + * 如果未发生变化,则返回空
  113 + *
  114 + * @param maxUpdateTime 当前短信模板的最大更新时间
  115 + * @return 短信模板列表
  116 + */
  117 + private List<SysSmsTemplateDO> loadSmsTemplateIfUpdate(Date maxUpdateTime) {
  118 + // 第一步,判断是否要更新。
  119 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
  120 + log.info("[loadSmsTemplateIfUpdate][首次加载全量短信模板]");
  121 + } else { // 判断数据库中是否有更新的短信模板
  122 + if (smsTemplateMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
  123 + return null;
  124 + }
  125 + log.info("[loadSmsTemplateIfUpdate][增量加载全量短信模板]");
  126 + }
  127 + // 第二步,如果有更新,则从数据库加载所有短信模板
  128 + return smsTemplateMapper.selectList();
  129 + }
  130 +
  131 + @Override
  132 + public SysSmsTemplateDO getSmsTemplateByCode(String code) {
  133 + return smsTemplateMapper.selectByCode(code);
  134 + }
  135 +
  136 + @Override
  137 + public SysSmsTemplateDO getSmsTemplateByCodeFromCache(String code) {
  138 + return smsTemplateCache.get(code);
  139 + }
  140 +
  141 + @Override
  142 + public String formatSmsTemplateContent(String content, Map<String, Object> params) {
  143 + return StrUtil.format(content, params);
  144 + }
  145 +
  146 + @VisibleForTesting
  147 + public List<String> parseTemplateContentParams(String content) {
  148 + return ReUtil.findAllGroup1(PATTERN_PARAMS, content);
  149 + }
  150 +
  151 + @Override
  152 + public Long createSmsTemplate(SysSmsTemplateCreateReqVO createReqVO) {
  153 + // 校验短信渠道
  154 + SysSmsChannelDO channelDO = checkSmsChannel(createReqVO.getChannelId());
  155 + // 校验短信编码是否重复
  156 + checkSmsTemplateCodeDuplicate(null, createReqVO.getCode());
  157 + // 校验短信模板
  158 + checkApiTemplate(createReqVO.getChannelId(), createReqVO.getApiTemplateId());
  159 +
  160 + // 插入
  161 + SysSmsTemplateDO template = SysSmsTemplateConvert.INSTANCE.convert(createReqVO);
  162 + template.setParams(parseTemplateContentParams(template.getContent()));
  163 + template.setChannelCode(channelDO.getCode());
  164 + smsTemplateMapper.insert(template);
  165 + // 发送刷新消息
  166 + smsProducer.sendSmsTemplateRefreshMessage();
  167 + // 返回
  168 + return template.getId();
  169 + }
  170 +
  171 + @Override
  172 + public void updateSmsTemplate(SysSmsTemplateUpdateReqVO updateReqVO) {
  173 + // 校验存在
  174 + this.validateSmsTemplateExists(updateReqVO.getId());
  175 + // 校验短信渠道
  176 + SysSmsChannelDO channelDO = checkSmsChannel(updateReqVO.getChannelId());
  177 + // 校验短信编码是否重复
  178 + checkSmsTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode());
  179 + // 校验短信模板
  180 + checkApiTemplate(updateReqVO.getChannelId(), updateReqVO.getApiTemplateId());
  181 +
  182 + // 更新
  183 + SysSmsTemplateDO updateObj = SysSmsTemplateConvert.INSTANCE.convert(updateReqVO);
  184 + updateObj.setParams(parseTemplateContentParams(updateObj.getContent()));
  185 + updateObj.setChannelCode(channelDO.getCode());
  186 + smsTemplateMapper.updateById(updateObj);
  187 + // 发送刷新消息
  188 + smsProducer.sendSmsTemplateRefreshMessage();
  189 + }
  190 +
  191 + @Override
  192 + public void deleteSmsTemplate(Long id) {
  193 + // 校验存在
  194 + this.validateSmsTemplateExists(id);
  195 + // 更新
  196 + smsTemplateMapper.deleteById(id);
  197 + // 发送刷新消息
  198 + smsProducer.sendSmsTemplateRefreshMessage();
  199 + }
  200 +
  201 + private void validateSmsTemplateExists(Long id) {
  202 + if (smsTemplateMapper.selectById(id) == null) {
  203 + throw exception(SMS_TEMPLATE_NOT_EXISTS);
  204 + }
  205 + }
  206 +
  207 + @Override
  208 + public SysSmsTemplateDO getSmsTemplate(Long id) {
  209 + return smsTemplateMapper.selectById(id);
  210 + }
  211 +
  212 + @Override
  213 + public List<SysSmsTemplateDO> getSmsTemplateList(Collection<Long> ids) {
  214 + return smsTemplateMapper.selectBatchIds(ids);
  215 + }
  216 +
  217 + @Override
  218 + public PageResult<SysSmsTemplateDO> getSmsTemplatePage(SysSmsTemplatePageReqVO pageReqVO) {
  219 + return smsTemplateMapper.selectPage(pageReqVO);
  220 + }
  221 +
  222 + @Override
  223 + public List<SysSmsTemplateDO> getSmsTemplateList(SysSmsTemplateExportReqVO exportReqVO) {
  224 + return smsTemplateMapper.selectList(exportReqVO);
  225 + }
  226 +
  227 + @Override
  228 + public Integer countByChannelId(Long channelId) {
  229 + return smsTemplateMapper.selectCountByChannelId(channelId);
  230 + }
  231 +
  232 + @VisibleForTesting
  233 + public SysSmsChannelDO checkSmsChannel(Long channelId) {
  234 + SysSmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId);
  235 + if (channelDO == null) {
  236 + throw exception(SMS_CHANNEL_NOT_EXISTS);
  237 + }
  238 + if (!Objects.equals(channelDO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
  239 + throw exception(SMS_CHANNEL_DISABLE);
  240 + }
  241 + return channelDO;
  242 + }
  243 +
  244 + @VisibleForTesting
  245 + public void checkSmsTemplateCodeDuplicate(Long id, String code) {
  246 + SysSmsTemplateDO template = smsTemplateMapper.selectByCode(code);
  247 + if (template == null) {
  248 + return;
  249 + }
  250 + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型
  251 + if (id == null) {
  252 + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code);
  253 + }
  254 + if (!template.getId().equals(id)) {
  255 + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code);
  256 + }
  257 + }
  258 +
  259 + /**
  260 + * 校验 API 短信平台的模板是否有效
  261 + *
  262 + * @param channelId 渠道编号
  263 + * @param apiTemplateId API 模板编号
  264 + */
  265 + @VisibleForTesting
  266 + public void checkApiTemplate(Long channelId, String apiTemplateId) {
  267 + // 获得短信模板
  268 + SmsClient smsClient = smsClientFactory.getSmsClient(channelId);
  269 + Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", channelId));
  270 + SmsCommonResult<SmsTemplateRespDTO> templateResult = smsClient.getSmsTemplate(apiTemplateId);
  271 + // 校验短信模板是否正确
  272 + templateResult.checkError();
  273 + }
  274 +
  275 +}
src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java
@@ -358,7 +358,7 @@ public class SysUserServiceImpl implements SysUserService { @@ -358,7 +358,7 @@ public class SysUserServiceImpl implements SysUserService {
358 } 358 }
359 // 如果存在,判断是否允许更新 359 // 如果存在,判断是否允许更新
360 if (!isUpdateSupport) { 360 if (!isUpdateSupport) {
361 - respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMessage()); 361 + respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg());
362 return; 362 return;
363 } 363 }
364 SysUserDO updateUser = SysUserConvert.INSTANCE.convert(importUser); 364 SysUserDO updateUser = SysUserConvert.INSTANCE.convert(importUser);
src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java
@@ -4,8 +4,10 @@ import cn.hutool.core.collection.CollUtil; @@ -4,8 +4,10 @@ import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.collection.CollectionUtil; 4 import cn.hutool.core.collection.CollectionUtil;
5 5
6 import java.util.*; 6 import java.util.*;
  7 +import java.util.function.BinaryOperator;
7 import java.util.function.Function; 8 import java.util.function.Function;
8 import java.util.function.Predicate; 9 import java.util.function.Predicate;
  10 +import java.util.function.Supplier;
9 import java.util.stream.Collectors; 11 import java.util.stream.Collectors;
10 12
11 /** 13 /**
@@ -30,6 +32,20 @@ public class CollectionUtils { @@ -30,6 +32,20 @@ public class CollectionUtils {
30 return from.stream().filter(predicate).collect(Collectors.toList()); 32 return from.stream().filter(predicate).collect(Collectors.toList());
31 } 33 }
32 34
  35 + public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) {
  36 + if (CollUtil.isEmpty(from)) {
  37 + return new ArrayList<>();
  38 + }
  39 + return distinct(from, keyMapper, (t1, t2) -> t1);
  40 + }
  41 +
  42 + public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) {
  43 + if (CollUtil.isEmpty(from)) {
  44 + return new ArrayList<>();
  45 + }
  46 + return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
  47 + }
  48 +
33 public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) { 49 public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
34 if (CollUtil.isEmpty(from)) { 50 if (CollUtil.isEmpty(from)) {
35 return new ArrayList<>(); 51 return new ArrayList<>();
@@ -48,30 +64,57 @@ public class CollectionUtils { @@ -48,30 +64,57 @@ public class CollectionUtils {
48 if (CollUtil.isEmpty(from)) { 64 if (CollUtil.isEmpty(from)) {
49 return new HashMap<>(); 65 return new HashMap<>();
50 } 66 }
51 - return from.stream().collect(Collectors.toMap(keyFunc, item -> item)); 67 + return convertMap(from, keyFunc, Function.identity());
  68 + }
  69 +
  70 + public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) {
  71 + if (CollUtil.isEmpty(from)) {
  72 + return supplier.get();
  73 + }
  74 + return convertMap(from, keyFunc, Function.identity(), supplier);
52 } 75 }
53 76
54 public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { 77 public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
55 if (CollUtil.isEmpty(from)) { 78 if (CollUtil.isEmpty(from)) {
56 return new HashMap<>(); 79 return new HashMap<>();
57 } 80 }
58 - return from.stream().collect(Collectors.toMap(keyFunc, valueFunc)); 81 + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1);
  82 + }
  83 +
  84 + public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) {
  85 + if (CollUtil.isEmpty(from)) {
  86 + return new HashMap<>();
  87 + }
  88 + return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new);
  89 + }
  90 +
  91 + public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) {
  92 + if (CollUtil.isEmpty(from)) {
  93 + return supplier.get();
  94 + }
  95 + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier);
  96 + }
  97 +
  98 + public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) {
  99 + if (CollUtil.isEmpty(from)) {
  100 + return new HashMap<>();
  101 + }
  102 + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier));
59 } 103 }
60 104
61 public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) { 105 public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) {
62 if (CollUtil.isEmpty(from)) { 106 if (CollUtil.isEmpty(from)) {
63 return new HashMap<>(); 107 return new HashMap<>();
64 } 108 }
65 - return from.stream().collect(Collectors.groupingBy(keyFunc,  
66 - Collectors.mapping(t -> t, Collectors.toList()))); 109 + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList())));
67 } 110 }
68 111
69 public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { 112 public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
70 if (CollUtil.isEmpty(from)) { 113 if (CollUtil.isEmpty(from)) {
71 return new HashMap<>(); 114 return new HashMap<>();
72 } 115 }
73 - return from.stream().collect(Collectors.groupingBy(keyFunc,  
74 - Collectors.mapping(valueFunc, Collectors.toList()))); 116 + return from.stream()
  117 + .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
75 } 118 }
76 119
77 // 暂时没想好名字,先以 2 结尾噶 120 // 暂时没想好名字,先以 2 结尾噶
src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java
@@ -2,6 +2,8 @@ package cn.iocoder.dashboard.util.collection; @@ -2,6 +2,8 @@ package cn.iocoder.dashboard.util.collection;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.collection.CollectionUtil; 4 import cn.hutool.core.collection.CollectionUtil;
  5 +import cn.iocoder.dashboard.common.core.KeyValue;
  6 +import com.google.common.collect.Maps;
5 import com.google.common.collect.Multimap; 7 import com.google.common.collect.Multimap;
6 8
7 import java.util.ArrayList; 9 import java.util.ArrayList;
@@ -55,4 +57,10 @@ public class MapUtils { @@ -55,4 +57,10 @@ public class MapUtils {
55 consumer.accept(value); 57 consumer.accept(value);
56 } 58 }
57 59
  60 + public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) {
  61 + Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size());
  62 + keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue()));
  63 + return map;
  64 + }
  65 +
58 } 66 }
src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java
@@ -9,6 +9,11 @@ import java.util.Date; @@ -9,6 +9,11 @@ import java.util.Date;
9 */ 9 */
10 public class DateUtils { 10 public class DateUtils {
11 11
  12 + /**
  13 + * 时区 - 默认
  14 + */
  15 + public static final String TIME_ZONE_DEFAULT = "GMT+8";
  16 +
12 public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; 17 public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
13 18
14 public static Date addTime(Duration duration) { 19 public static Date addTime(Duration duration) {
src/main/java/cn/iocoder/dashboard/util/json/JsonUtils.java
@@ -7,7 +7,8 @@ import com.fasterxml.jackson.core.type.TypeReference; @@ -7,7 +7,8 @@ import com.fasterxml.jackson.core.type.TypeReference;
7 import com.fasterxml.jackson.databind.ObjectMapper; 7 import com.fasterxml.jackson.databind.ObjectMapper;
8 8
9 import java.io.IOException; 9 import java.io.IOException;
10 -import java.util.Set; 10 +import java.util.ArrayList;
  11 +import java.util.List;
11 12
12 /** 13 /**
13 * JSON 工具类 14 * JSON 工具类
@@ -20,7 +21,7 @@ public class JsonUtils { @@ -20,7 +21,7 @@ public class JsonUtils {
20 21
21 /** 22 /**
22 * 初始化 objectMapper 属性 23 * 初始化 objectMapper 属性
23 - * 24 + * <p>
24 * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean 25 * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
25 * 26 *
26 * @param objectMapper ObjectMapper 对象 27 * @param objectMapper ObjectMapper 对象
@@ -59,7 +60,7 @@ public class JsonUtils { @@ -59,7 +60,7 @@ public class JsonUtils {
59 } 60 }
60 } 61 }
61 62
62 - public static Object parseObject(String text, TypeReference<Set<Long>> typeReference) { 63 + public static <T> T parseObject(String text, TypeReference<T> typeReference) {
63 try { 64 try {
64 return objectMapper.readValue(text, typeReference); 65 return objectMapper.readValue(text, typeReference);
65 } catch (IOException e) { 66 } catch (IOException e) {
@@ -67,4 +68,15 @@ public class JsonUtils { @@ -67,4 +68,15 @@ public class JsonUtils {
67 } 68 }
68 } 69 }
69 70
  71 + public static <T> List<T> parseArray(String text, Class<T> clazz) {
  72 + if (StrUtil.isEmpty(text)) {
  73 + return new ArrayList<>();
  74 + }
  75 + try {
  76 + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
  77 + } catch (IOException e) {
  78 + throw new RuntimeException(e);
  79 + }
  80 + }
  81 +
70 } 82 }
src/main/java/cn/iocoder/dashboard/util/string/StrUtils.java
1 package cn.iocoder.dashboard.util.string; 1 package cn.iocoder.dashboard.util.string;
2 2
  3 +import cn.hutool.core.util.ObjectUtil;
3 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
4 5
  6 +import java.util.Map;
  7 +
5 /** 8 /**
6 * 字符串工具类 9 * 字符串工具类
7 * 10 *
@@ -13,4 +16,22 @@ public class StrUtils { @@ -13,4 +16,22 @@ public class StrUtils {
13 return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 16 return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
14 } 17 }
15 18
  19 + /**
  20 + * 指定字符串的
  21 + * @param str
  22 + * @param replaceMap
  23 + * @return
  24 + */
  25 + public static String replace(String str, Map<String, String> replaceMap) {
  26 + assert StrUtil.isNotBlank(str);
  27 + if (ObjectUtil.isEmpty(replaceMap)) {
  28 + return str;
  29 + }
  30 + String result = null;
  31 + for (String key : replaceMap.keySet()) {
  32 + result = str.replace(key, replaceMap.get(key));
  33 + }
  34 + return result;
  35 + }
  36 +
16 } 37 }
src/main/resources/codegen/java/service/service.vm
@@ -63,7 +63,7 @@ public interface ${table.className}Service { @@ -63,7 +63,7 @@ public interface ${table.className}Service {
63 * 获得${table.classComment}列表, 用于 Excel 导出 63 * 获得${table.classComment}列表, 用于 Excel 导出
64 * 64 *
65 * @param exportReqVO 查询条件 65 * @param exportReqVO 查询条件
66 - * @return ${table.classComment}分页 66 + * @return ${table.classComment}列表
67 */ 67 */
68 List<${table.className}DO> get${simpleClassName}List(${table.className}ExportReqVO exportReqVO); 68 List<${table.className}DO> get${simpleClassName}List(${table.className}ExportReqVO exportReqVO);
69 69
src/main/resources/codegen/vue/views/index.vue.vm
@@ -76,7 +76,7 @@ @@ -76,7 +76,7 @@
76 <template slot-scope="scope"> 76 <template slot-scope="scope">
77 <span>{{ getDictDataLabel(DICT_TYPE.$dictType.toUpperCase(), scope.row.${column.javaField}) }}</span> 77 <span>{{ getDictDataLabel(DICT_TYPE.$dictType.toUpperCase(), scope.row.${column.javaField}) }}</span>
78 </template> 78 </template>
79 - </el-table-column>> 79 + </el-table-column>
80 #else 80 #else
81 <el-table-column label="${comment}" align="center" prop="${javaField}" /> 81 <el-table-column label="${comment}" align="center" prop="${javaField}" />
82 #end 82 #end
@@ -137,7 +137,7 @@ @@ -137,7 +137,7 @@
137 </el-select> 137 </el-select>
138 </el-form-item> 138 </el-form-item>
139 #elseif($column.htmlType == "checkbox")## 多选框 139 #elseif($column.htmlType == "checkbox")## 多选框
140 - <el-form-item label="${comment}"> 140 + <el-form-item label="${comment}" prop="${javaField}">
141 <el-checkbox-group v-model="form.${javaField}"> 141 <el-checkbox-group v-model="form.${javaField}">
142 #if ("" != $dictType)## 有数据字典 142 #if ("" != $dictType)## 有数据字典
143 <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())" 143 <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
@@ -148,7 +148,7 @@ @@ -148,7 +148,7 @@
148 </el-checkbox-group> 148 </el-checkbox-group>
149 </el-form-item> 149 </el-form-item>
150 #elseif($column.htmlType == "radio")## 单选框 150 #elseif($column.htmlType == "radio")## 单选框
151 - <el-form-item label="${comment}"> 151 + <el-form-item label="${comment}" prop="${javaField}">
152 <el-radio-group v-model="form.${javaField}"> 152 <el-radio-group v-model="form.${javaField}">
153 #if ("" != $dictType)## 有数据字典 153 #if ("" != $dictType)## 有数据字典
154 <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())" 154 <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
src/test-integration/java/cn/iocoder/dashboard/BaseDbAndRedisIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.dashboard;
  2 +
  3 +import cn.iocoder.dashboard.framework.datasource.config.DataSourceConfiguration;
  4 +import cn.iocoder.dashboard.framework.mybatis.config.MybatisConfiguration;
  5 +import cn.iocoder.dashboard.framework.redis.config.RedisConfig;
  6 +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
  7 +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
  8 +import org.redisson.spring.starter.RedissonAutoConfiguration;
  9 +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
  10 +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  11 +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
  12 +import org.springframework.boot.test.context.SpringBootTest;
  13 +import org.springframework.context.annotation.Import;
  14 +import org.springframework.test.context.ActiveProfiles;
  15 +
  16 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class)
  17 +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
  18 +public class BaseDbAndRedisIntegrationTest {
  19 +
  20 + @Import({
  21 + // DB 配置类
  22 + DataSourceConfiguration.class, // 自己的 DB 配置类
  23 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类
  24 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
  25 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类
  26 + // MyBatis 配置类
  27 + MybatisConfiguration.class, // 自己的 MyBatis 配置类
  28 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
  29 + // Redis 配置类
  30 + RedisAutoConfiguration.class, // Spring Redis 自动配置类
  31 + RedisConfig.class, // 自己的 Redis 配置类
  32 + RedissonAutoConfiguration.class, // Redisson 自动高配置类
  33 + })
  34 + public static class Application {
  35 + }
  36 +
  37 +}
src/test-integration/java/cn/iocoder/dashboard/framework/redis/core/stream/RedisStreamTest.java
@@ -43,7 +43,7 @@ public class RedisStreamTest { @@ -43,7 +43,7 @@ public class RedisStreamTest {
43 for (int i = 0; i < 100; i++) { 43 for (int i = 0; i < 100; i++) {
44 // 创建消息 44 // 创建消息
45 SysSmsSendMessage message = new SysSmsSendMessage(); 45 SysSmsSendMessage message = new SysSmsSendMessage();
46 - message.setMobile("15601691300").setTemplateCode("test:" + i); 46 + message.setMobile("15601691300").setApiTemplateId("test:" + i);
47 // 发送消息 47 // 发送消息
48 RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message); 48 RedisMessageUtils.sendStreamMessage(stringRedisTemplate, message);
49 } 49 }
src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
  2 +
  3 +import cn.iocoder.dashboard.common.core.KeyValue;
  4 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  5 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  6 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  7 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
  8 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  9 +import org.junit.jupiter.api.BeforeAll;
  10 +import org.junit.jupiter.api.Test;
  11 +
  12 +import java.util.ArrayList;
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * {@link AliyunSmsClient} 的集成测试
  17 + */
  18 +public class AliyunSmsClientIntegrationTest {
  19 +
  20 + private static AliyunSmsClient smsClient;
  21 +
  22 + @BeforeAll
  23 + public static void before() {
  24 + // 创建配置类
  25 + SmsChannelProperties properties = new SmsChannelProperties();
  26 + properties.setId(1L);
  27 + properties.setSignature("Ballcat");
  28 + properties.setCode(SmsChannelEnum.ALIYUN.getCode());
  29 + properties.setApiKey(System.getenv("ALIYUN_ACCESS_KEY"));
  30 + properties.setApiSecret(System.getenv("ALIYUN_SECRET_KEY"));
  31 + // 创建客户端
  32 + smsClient = new AliyunSmsClient(properties);
  33 + smsClient.init();
  34 + }
  35 +
  36 + @Test
  37 + public void testSendSms() {
  38 + List<KeyValue<String, Object>> templateParams = new ArrayList<>();
  39 + templateParams.add(new KeyValue<>("code", "1024"));
  40 +// templateParams.put("operation", "嘿嘿");
  41 +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
  42 + SmsCommonResult<SmsSendRespDTO> result = smsClient.sendSms(1L, "15601691399",
  43 + "SMS_207945135", templateParams);
  44 + System.out.println(result);
  45 + }
  46 +
  47 + @Test
  48 + public void testGetSmsTemplate() {
  49 + String apiTemplateId = "SMS_2079451351";
  50 + SmsCommonResult<SmsTemplateRespDTO> result = smsClient.getSmsTemplate(apiTemplateId);
  51 + System.out.println(result);
  52 + }
  53 +
  54 +}
src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.debug;
  2 +
  3 +import cn.iocoder.dashboard.common.core.KeyValue;
  4 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  5 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  6 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
  7 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  8 +import org.junit.jupiter.api.BeforeAll;
  9 +import org.junit.jupiter.api.Test;
  10 +
  11 +import java.util.ArrayList;
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * {@link DebugDingTalkSmsClient} 的集成测试
  16 + */
  17 +public class DebugDingTalkSmsClientIntegrationTest {
  18 +
  19 + private static DebugDingTalkSmsClient smsClient;
  20 +
  21 + @BeforeAll
  22 + public static void init() {
  23 + // 创建配置类
  24 + SmsChannelProperties properties = new SmsChannelProperties();
  25 + properties.setId(1L);
  26 + properties.setSignature("芋道");
  27 + properties.setCode(SmsChannelEnum.DEBUG_DING_TALK.getCode());
  28 + properties.setApiKey("696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859");
  29 + properties.setApiSecret("SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67");
  30 + // 创建客户端
  31 + smsClient = new DebugDingTalkSmsClient(properties);
  32 + smsClient.init();
  33 + }
  34 +
  35 + @Test
  36 + public void testSendSms() {
  37 + List<KeyValue<String, Object>> templateParams = new ArrayList<>();
  38 + templateParams.add(new KeyValue<>("code", "1024"));
  39 + templateParams.add(new KeyValue<>("operation", "嘿嘿"));
  40 +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
  41 + SmsCommonResult<SmsSendRespDTO> result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams);
  42 + System.out.println(result);
  43 + }
  44 +
  45 +}
src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
  2 +
  3 +import cn.iocoder.dashboard.common.core.KeyValue;
  4 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  5 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  6 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  7 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
  8 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  9 +import org.junit.jupiter.api.BeforeAll;
  10 +import org.junit.jupiter.api.Test;
  11 +
  12 +import java.util.ArrayList;
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * {@link YunpianSmsClient} 的集成测试
  17 + */
  18 +public class YunpianSmsClientIntegrationTest {
  19 +
  20 + private static YunpianSmsClient smsClient;
  21 +
  22 + @BeforeAll
  23 + public static void init() {
  24 + // 创建配置类
  25 + SmsChannelProperties properties = new SmsChannelProperties();
  26 + properties.setId(1L);
  27 + properties.setSignature("芋道");
  28 + properties.setCode(SmsChannelEnum.YUN_PIAN.getCode());
  29 + properties.setApiKey("1555a14277cb8a608cf45a9e6a80d510");
  30 + // 创建客户端
  31 + smsClient = new YunpianSmsClient(properties);
  32 + smsClient.init();
  33 + }
  34 +
  35 + @Test
  36 + public void testSendSms() {
  37 + List<KeyValue<String, Object>> templateParams = new ArrayList<>();
  38 + templateParams.add(new KeyValue<>("code", "1024"));
  39 + templateParams.add(new KeyValue<>("operation", "嘿嘿"));
  40 +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
  41 + SmsCommonResult<SmsSendRespDTO> result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams);
  42 + System.out.println(result);
  43 + }
  44 +
  45 + @Test
  46 + public void testGetSmsTemplate() {
  47 + String apiTemplateId = "4383920";
  48 + SmsCommonResult<SmsTemplateRespDTO> result = smsClient.getSmsTemplate(apiTemplateId);
  49 + System.out.println(result);
  50 + }
  51 +
  52 +}
src/test-integration/java/cn/iocoder/dashboard/framework/sms/core/client/package-info.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client;
src/main/java/cn/iocoder/dashboard/modules/system/service/package-info.java renamed to src/test-integration/java/cn/iocoder/dashboard/modules/system/service/package-info.java
src/test-integration/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsServiceIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.thread.ThreadUtil;
  5 +import cn.iocoder.dashboard.BaseDbAndRedisIntegrationTest;
  6 +import cn.iocoder.dashboard.common.enums.UserTypeEnum;
  7 +import cn.iocoder.dashboard.framework.sms.config.SmsConfiguration;
  8 +import cn.iocoder.dashboard.modules.system.mq.consumer.sms.SysSmsSendConsumer;
  9 +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
  10 +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsChannelServiceImpl;
  11 +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsLogServiceImpl;
  12 +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsServiceImpl;
  13 +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsTemplateServiceImpl;
  14 +import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
  15 +import org.junit.jupiter.api.Test;
  16 +import org.springframework.boot.test.mock.mockito.MockBean;
  17 +import org.springframework.context.annotation.Import;
  18 +
  19 +import javax.annotation.Resource;
  20 +import java.util.Map;
  21 +import java.util.concurrent.TimeUnit;
  22 +
  23 +@Import({SmsConfiguration.class,
  24 + SysSmsChannelServiceImpl.class, SysSmsServiceImpl.class, SysSmsTemplateServiceImpl.class, SysSmsLogServiceImpl.class,
  25 + SysSmsProducer.class, SysSmsSendConsumer.class})
  26 +public class SysSmsServiceIntegrationTest extends BaseDbAndRedisIntegrationTest {
  27 +
  28 + @Resource
  29 + private SysSmsServiceImpl smsService;
  30 + @Resource
  31 + private SysSmsChannelServiceImpl smsChannelService;
  32 +
  33 + @MockBean
  34 + private SysUserService userService;
  35 +
  36 + @Test
  37 + public void testSendSingleSms_yunpianSuccess() {
  38 + // 参数准备
  39 + String mobile = "15601691399";
  40 + Long userId = 1L;
  41 + Integer userType = UserTypeEnum.ADMIN.getValue();
  42 + String templateCode = "test_01";
  43 + Map<String, Object> templateParams = MapUtil.<String, Object>builder()
  44 + .put("operation", "登陆").put("code", "1234").build();
  45 + // 调用
  46 + smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams);
  47 +
  48 + // 等待 MQ 消费
  49 + ThreadUtil.sleep(1, TimeUnit.HOURS);
  50 + }
  51 +
  52 + @Test
  53 + public void testSendSingleSms_aliyunSuccess() {
  54 + // 参数准备
  55 + String mobile = "15601691399";
  56 + Long userId = 1L;
  57 + Integer userType = UserTypeEnum.ADMIN.getValue();
  58 + String templateCode = "test_02";
  59 + Map<String, Object> templateParams = MapUtil.<String, Object>builder()
  60 + .put("code", "1234").build();
  61 + // 调用
  62 + smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams);
  63 +
  64 + // 等待 MQ 消费
  65 + ThreadUtil.sleep(1, TimeUnit.HOURS);
  66 + }
  67 +
  68 +// @Test
  69 +// public void testDoSendSms() {
  70 +// // 等待 MQ 消费
  71 +// ThreadUtil.sleep(1, TimeUnit.HOURS);
  72 +// }
  73 +
  74 +}
src/test-integration/resources/application-integration-test.yaml
@@ -9,19 +9,15 @@ spring: @@ -9,19 +9,15 @@ spring:
9 # 数据源配置项 9 # 数据源配置项
10 datasource: 10 datasource:
11 name: ruoyi-vue-pro 11 name: ruoyi-vue-pro
12 - url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写  
13 - driver-class-name: org.h2.Driver  
14 - username: sa  
15 - password:  
16 - schema: classpath:sql/create_tables.sql # MySQL 转 H2 的语句,使用 https://www.jooq.org/translate/ 工具  
17 - druid:  
18 - async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度  
19 - initial-size: 1 # 单元测试,配置为 1,提升启动速度 12 + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
  13 + driver-class-name: com.mysql.jdbc.Driver
  14 + username: root
  15 + password: 123456
20 16
21 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 17 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
22 redis: 18 redis:
23 host: 127.0.0.1 # 地址 19 host: 127.0.0.1 # 地址
24 - port: 6379 # 端口(单元测试,使用 16379 端口) 20 + port: 6379 # 端口
25 database: 0 # 数据库索引 21 database: 0 # 数据库索引
26 22
27 mybatis: 23 mybatis:
src/test/java/cn/iocoder/dashboard/BaseMockitoUnitTest.java 0 → 100644
  1 +package cn.iocoder.dashboard;
  2 +
  3 +import org.junit.jupiter.api.extension.ExtendWith;
  4 +import org.mockito.junit.jupiter.MockitoExtension;
  5 +
  6 +/**
  7 + * 纯 Mockito 的单元测试
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@ExtendWith(MockitoExtension.class)
  12 +public class BaseMockitoUnitTest {
  13 +}
src/test/java/cn/iocoder/dashboard/framework/package-info.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework;
src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
  2 +
  3 +import cn.hutool.core.util.ReflectUtil;
  4 +import cn.iocoder.dashboard.BaseMockitoUnitTest;
  5 +import cn.iocoder.dashboard.common.core.KeyValue;
  6 +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
  7 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  8 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
  9 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  10 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  11 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  12 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  13 +import cn.iocoder.dashboard.util.collection.MapUtils;
  14 +import cn.iocoder.dashboard.util.date.DateUtils;
  15 +import com.aliyuncs.AcsRequest;
  16 +import com.aliyuncs.IAcsClient;
  17 +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
  18 +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse;
  19 +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
  20 +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
  21 +import com.aliyuncs.exceptions.ClientException;
  22 +import com.google.common.collect.Lists;
  23 +import org.junit.jupiter.api.Test;
  24 +import org.mockito.ArgumentMatcher;
  25 +import org.mockito.InjectMocks;
  26 +import org.mockito.Mock;
  27 +
  28 +import java.util.List;
  29 +import java.util.function.Function;
  30 +
  31 +import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR;
  32 +import static cn.iocoder.dashboard.util.RandomUtils.*;
  33 +import static cn.iocoder.dashboard.util.json.JsonUtils.toJsonString;
  34 +import static org.junit.jupiter.api.Assertions.*;
  35 +import static org.mockito.ArgumentMatchers.any;
  36 +import static org.mockito.ArgumentMatchers.argThat;
  37 +import static org.mockito.Mockito.when;
  38 +
  39 +/**
  40 + * {@link AliyunSmsClient} 的单元测试
  41 + *
  42 + * @author 芋道源码
  43 + */
  44 +public class AliyunSmsClientTest extends BaseMockitoUnitTest {
  45 +
  46 + private final SmsChannelProperties properties = new SmsChannelProperties()
  47 + .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错
  48 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
  49 + .setSignature("芋道源码");
  50 +
  51 + @InjectMocks
  52 + private final AliyunSmsClient smsClient = new AliyunSmsClient(properties);
  53 +
  54 + @Mock
  55 + private IAcsClient client;
  56 +
  57 + @Test
  58 + public void testDoInit() {
  59 + // 准备参数
  60 + // mock 方法
  61 +
  62 + // 调用
  63 + smsClient.doInit();
  64 + // 断言
  65 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "acsClient"));
  66 + }
  67 +
  68 + @Test
  69 + @SuppressWarnings("unchecked")
  70 + public void testDoSendSms() throws ClientException {
  71 + // 准备参数
  72 + Long sendLogId = randomLongId();
  73 + String mobile = randomString();
  74 + String apiTemplateId = randomString();
  75 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  76 + new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
  77 + // mock 方法
  78 + SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK"));
  79 + when(client.getAcsResponse(argThat((ArgumentMatcher<SendSmsRequest>) acsRequest -> {
  80 + assertEquals(mobile, acsRequest.getPhoneNumbers());
  81 + assertEquals(properties.getSignature(), acsRequest.getSignName());
  82 + assertEquals(apiTemplateId, acsRequest.getTemplateCode());
  83 + assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam());
  84 + assertEquals(sendLogId.toString(), acsRequest.getOutId());
  85 + return true;
  86 + }))).thenReturn(response);
  87 +
  88 + // 调用
  89 + SmsCommonResult<SmsSendRespDTO> result = smsClient.doSendSms(sendLogId, mobile,
  90 + apiTemplateId, templateParams);
  91 + // 断言
  92 + assertEquals(response.getCode(), result.getApiCode());
  93 + assertEquals(response.getMessage(), result.getApiMsg());
  94 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
  95 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
  96 + assertEquals(response.getRequestId(), result.getApiRequestId());
  97 + // 断言结果
  98 + assertEquals(response.getBizId(), result.getData().getSerialNo());
  99 + }
  100 +
  101 + @Test
  102 + public void testDoTParseSmsReceiveStatus() throws Throwable {
  103 + // 准备参数
  104 + String text = "[\n" +
  105 + " {\n" +
  106 + " \"phone_number\" : \"13900000001\",\n" +
  107 + " \"send_time\" : \"2017-01-01 11:12:13\",\n" +
  108 + " \"report_time\" : \"2017-02-02 22:23:24\",\n" +
  109 + " \"success\" : true,\n" +
  110 + " \"err_code\" : \"DELIVERED\",\n" +
  111 + " \"err_msg\" : \"用户接收成功\",\n" +
  112 + " \"sms_size\" : \"1\",\n" +
  113 + " \"biz_id\" : \"12345\",\n" +
  114 + " \"out_id\" : \"67890\"\n" +
  115 + " }\n" +
  116 + "]";
  117 + // mock 方法
  118 +
  119 + // 调用
  120 + List<SmsReceiveRespDTO> statuses = smsClient.doParseSmsReceiveStatus(text);
  121 + // 断言
  122 + assertEquals(1, statuses.size());
  123 + assertTrue(statuses.get(0).getSuccess());
  124 + assertEquals("DELIVERED", statuses.get(0).getErrorCode());
  125 + assertEquals("用户接收成功", statuses.get(0).getErrorMsg());
  126 + assertEquals("13900000001", statuses.get(0).getMobile());
  127 + assertEquals(DateUtils.buildTime(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime());
  128 + assertEquals("12345", statuses.get(0).getSerialNo());
  129 + assertEquals(67890L, statuses.get(0).getLogId());
  130 + }
  131 +
  132 + @Test
  133 + public void testDoGetSmsTemplate() throws ClientException {
  134 + // 准备参数
  135 + String apiTemplateId = randomString();
  136 + // mock 方法
  137 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> {
  138 + o.setCode("OK");
  139 + o.setTemplateStatus(1); // 设置模板通过
  140 + });
  141 + when(client.getAcsResponse(argThat((ArgumentMatcher<QuerySmsTemplateRequest>) acsRequest -> {
  142 + assertEquals(apiTemplateId, acsRequest.getTemplateCode());
  143 + return true;
  144 + }))).thenReturn(response);
  145 +
  146 + // 调用
  147 + SmsCommonResult<SmsTemplateRespDTO> result = smsClient.doGetSmsTemplate(apiTemplateId);
  148 + // 断言
  149 + assertEquals(response.getCode(), result.getApiCode());
  150 + assertEquals(response.getMessage(), result.getApiMsg());
  151 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
  152 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
  153 + assertEquals(response.getRequestId(), result.getApiRequestId());
  154 + // 断言结果
  155 + assertEquals(response.getTemplateCode(), result.getData().getId());
  156 + assertEquals(response.getTemplateContent(), result.getData().getContent());
  157 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
  158 + assertEquals(response.getReason(), result.getData().getAuditReason());
  159 + }
  160 +
  161 + @Test
  162 + public void testConvertSmsTemplateAuditStatus() {
  163 + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
  164 + smsClient.convertSmsTemplateAuditStatus(0));
  165 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
  166 + smsClient.convertSmsTemplateAuditStatus(1));
  167 + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
  168 + smsClient.convertSmsTemplateAuditStatus(2));
  169 + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3),
  170 + "未知审核状态(3)");
  171 + }
  172 +
  173 + @Test
  174 + @SuppressWarnings("unchecked")
  175 + public void testInvoke_throwable() throws ClientException {
  176 + // 准备参数
  177 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
  178 + // mock 方法
  179 + ClientException ex = new ClientException("isv.INVALID_PARAMETERS", "参数不正确", randomString());
  180 + when(client.getAcsResponse(any(AcsRequest.class))).thenThrow(ex);
  181 +
  182 + // 调用,并断言异常
  183 + SmsCommonResult<?> result = smsClient.invoke(request,null);
  184 + // 断言
  185 + assertEquals(ex.getErrCode(), result.getApiCode());
  186 + assertEquals(ex.getErrMsg(), result.getApiMsg());
  187 + assertEquals(SMS_API_PARAM_ERROR.getCode(), result.getCode());
  188 + assertEquals(SMS_API_PARAM_ERROR.getMsg(), result.getMsg());
  189 + assertEquals(ex.getRequestId(), result.getApiRequestId());
  190 + }
  191 +
  192 + @Test
  193 + public void testInvoke_success() throws ClientException {
  194 + // 准备参数
  195 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
  196 + Function<QuerySmsTemplateResponse, SmsTemplateRespDTO> responseConsumer = response -> {
  197 + SmsTemplateRespDTO data = new SmsTemplateRespDTO();
  198 + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
  199 + data.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(response.getReason());
  200 + return data;
  201 + };
  202 + // mock 方法
  203 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> {
  204 + o.setCode("OK");
  205 + o.setTemplateStatus(1); // 设置模板通过
  206 + });
  207 + when(client.getAcsResponse(any(AcsRequest.class))).thenReturn(response);
  208 +
  209 + // 调用
  210 + SmsCommonResult<SmsTemplateRespDTO> result = smsClient.invoke(request, responseConsumer);
  211 + // 断言
  212 + assertEquals(response.getCode(), result.getApiCode());
  213 + assertEquals(response.getMessage(), result.getApiMsg());
  214 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
  215 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
  216 + assertEquals(response.getRequestId(), result.getApiRequestId());
  217 + // 断言结果
  218 + assertEquals(response.getTemplateCode(), result.getData().getId());
  219 + assertEquals(response.getTemplateContent(), result.getData().getContent());
  220 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
  221 + assertEquals(response.getReason(), result.getData().getAuditReason());
  222 + }
  223 +
  224 +}
src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
  2 +
  3 +import cn.iocoder.dashboard.BaseMockitoUnitTest;
  4 +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
  5 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
  6 +import org.junit.jupiter.api.Test;
  7 +import org.mockito.InjectMocks;
  8 +
  9 +import static org.junit.jupiter.api.Assertions.*;
  10 +
  11 +/**
  12 + * {@link AliyunSmsCodeMapping} 的单元测试
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +public class AliyunSmsCodeMappingTest extends BaseMockitoUnitTest {
  17 +
  18 + @InjectMocks
  19 + private AliyunSmsCodeMapping codeMapping;
  20 +
  21 + @Test
  22 + public void testApply() {
  23 + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("OK"));
  24 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("MissingAccessKeyId"));
  25 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_NOT_EXISTS"));
  26 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_ABNORMAL"));
  27 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("isv.DAY_LIMIT_CONTROL"));
  28 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("isv.SMS_CONTENT_ILLEGAL"));
  29 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGN_ILLEGAL"));
  30 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SIGN_NAME_ILLEGAL"));
  31 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("isp.RAM_PERMISSION_DENY"));
  32 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.OUT_OF_SERVICE"));
  33 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.AMOUNT_NOT_ENOUGH"));
  34 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("isv.SMS_TEMPLATE_ILLEGAL"));
  35 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGNATURE_ILLEGAL"));
  36 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_PARAMETERS"));
  37 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_JSON_PARAM"));
  38 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("isv.MOBILE_NUMBER_ILLEGAL"));
  39 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("isv.TEMPLATE_MISSING_PARAMETERS"));
  40 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("isv.BUSINESS_LIMIT_CONTROL"));
  41 + }
  42 +
  43 +}
src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsClientTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
  2 +
  3 +import cn.hutool.core.util.ReflectUtil;
  4 +import cn.iocoder.dashboard.BaseMockitoUnitTest;
  5 +import cn.iocoder.dashboard.common.core.KeyValue;
  6 +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
  7 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  8 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
  9 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  10 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  11 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
  12 +import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
  13 +import cn.iocoder.dashboard.util.date.DateUtils;
  14 +import com.google.common.collect.Lists;
  15 +import com.yunpian.sdk.YunpianClient;
  16 +import com.yunpian.sdk.api.SmsApi;
  17 +import com.yunpian.sdk.api.TplApi;
  18 +import com.yunpian.sdk.constant.YunpianConstant;
  19 +import com.yunpian.sdk.model.Result;
  20 +import com.yunpian.sdk.model.SmsSingleSend;
  21 +import com.yunpian.sdk.model.Template;
  22 +import org.junit.jupiter.api.Test;
  23 +import org.mockito.InjectMocks;
  24 +import org.mockito.Mock;
  25 +
  26 +import java.util.HashMap;
  27 +import java.util.List;
  28 +import java.util.Map;
  29 +import java.util.function.Function;
  30 +import java.util.function.Supplier;
  31 +
  32 +import static cn.iocoder.dashboard.util.RandomUtils.*;
  33 +import static com.yunpian.sdk.constant.Code.OK;
  34 +import static org.junit.jupiter.api.Assertions.*;
  35 +import static org.mockito.Mockito.*;
  36 +
  37 +/**
  38 + * 对 {@link YunpianSmsClient} 的单元测试
  39 + *
  40 + * @author 芋道源码
  41 + */
  42 +public class YunpianSmsClientTest extends BaseMockitoUnitTest {
  43 +
  44 + private final SmsChannelProperties properties = new SmsChannelProperties()
  45 + .setApiKey(randomString()); // 随机一个 apiKey,避免构建报错
  46 +
  47 + @InjectMocks
  48 + private final YunpianSmsClient smsClient = new YunpianSmsClient(properties);
  49 +
  50 + @Mock
  51 + private YunpianClient client;
  52 +
  53 + @Test
  54 + public void testDoInit() {
  55 + // 准备参数
  56 + // mock 方法
  57 +
  58 + // 调用
  59 + smsClient.doInit();
  60 + // 断言
  61 + assertNotEquals(client, ReflectUtil.getFieldValue(smsClient, "client"));
  62 + verify(client, times(1)).close();
  63 + }
  64 +
  65 + @Test
  66 + @SuppressWarnings("unchecked")
  67 + public void testDoSendSms() throws Throwable {
  68 + // 准备参数
  69 + Long sendLogId = randomLongId();
  70 + String mobile = randomString();
  71 + String apiTemplateId = randomString();
  72 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  73 + new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
  74 + // mock sms 方法
  75 + SmsApi smsApi = mock(SmsApi.class);
  76 + when(client.sms()).thenReturn(smsApi);
  77 + // mock tpl_single_send 方法
  78 + Map<String, String> request = new HashMap<>();
  79 + request.put(YunpianConstant.MOBILE, mobile);
  80 + request.put(YunpianConstant.TPL_ID, apiTemplateId);
  81 + request.put(YunpianConstant.TPL_VALUE, "#code#=1234&#op#=login");
  82 + request.put(YunpianConstant.UID, String.valueOf(sendLogId));
  83 + request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl());
  84 + Result<SmsSingleSend> responseResult = randomPojo(Result.class, SmsSingleSend.class,
  85 + o -> o.setCode(OK)); // API 发送成功的 code
  86 + when(smsApi.tpl_single_send(eq(request))).thenReturn(responseResult);
  87 +
  88 + // 调用
  89 + SmsCommonResult<SmsSendRespDTO> result = smsClient.doSendSms(sendLogId, mobile,
  90 + apiTemplateId, templateParams);
  91 + // 断言
  92 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode());
  93 + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg());
  94 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
  95 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
  96 + assertNull(result.getApiRequestId());
  97 + // 断言结果
  98 + assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo());
  99 + }
  100 +
  101 + @Test
  102 + public void testDoParseSmsReceiveStatus() throws Throwable {
  103 + // 准备参数
  104 + String text = "[{\"sid\":9527,\"uid\":1024,\"user_receive_time\":\"2014-03-17 22:55:21\",\"error_msg\":\"\",\"mobile\":\"15205201314\",\"report_status\":\"SUCCESS\"}]";
  105 + // mock 方法
  106 +
  107 + // 调用
  108 +
  109 + // 断言
  110 + // 调用
  111 + List<SmsReceiveRespDTO> statuses = smsClient.doParseSmsReceiveStatus(text);
  112 + // 断言
  113 + assertEquals(1, statuses.size());
  114 + assertTrue(statuses.get(0).getSuccess());
  115 + assertEquals("", statuses.get(0).getErrorCode());
  116 + assertNull(statuses.get(0).getErrorMsg());
  117 + assertEquals("15205201314", statuses.get(0).getMobile());
  118 + assertEquals(DateUtils.buildTime(2014, 3, 17, 22, 55, 21), statuses.get(0).getReceiveTime());
  119 + assertEquals("9527", statuses.get(0).getSerialNo());
  120 + assertEquals(1024L, statuses.get(0).getLogId());
  121 + }
  122 +
  123 + @Test
  124 + @SuppressWarnings("unchecked")
  125 + public void testDoGetSmsTemplate() throws Throwable {
  126 + // 准备参数
  127 + String apiTemplateId = randomString();
  128 + // mock tpl 方法
  129 + TplApi tplApi = mock(TplApi.class);
  130 + when(client.tpl()).thenReturn(tplApi);
  131 + // mock get 方法
  132 + Map<String, String> request = new HashMap<>();
  133 + request.put(YunpianConstant.APIKEY, properties.getApiKey());
  134 + request.put(YunpianConstant.TPL_ID, apiTemplateId);
  135 + Result<List<Template>> responseResult = randomPojo(Result.class, List.class, o -> {
  136 + o.setCode(OK); // API 发送成功的 code
  137 + o.setData(randomPojoList(Template.class, t -> t.setCheck_status("SUCCESS")));
  138 + });
  139 + when(tplApi.get(eq(request))).thenReturn(responseResult);
  140 +
  141 + // 调用
  142 + SmsCommonResult<SmsTemplateRespDTO> result = smsClient.doGetSmsTemplate(apiTemplateId);
  143 + // 断言
  144 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode());
  145 + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg());
  146 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
  147 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
  148 + assertNull(result.getApiRequestId());
  149 + // 断言结果
  150 + Template template = responseResult.getData().get(0);
  151 + assertEquals(template.getTpl_id().toString(), result.getData().getId());
  152 + assertEquals(template.getTpl_content(), result.getData().getContent());
  153 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
  154 + assertEquals(template.getReason(), result.getData().getAuditReason());
  155 + }
  156 +
  157 + @Test
  158 + public void testConvertSmsTemplateAuditStatus() {
  159 + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
  160 + smsClient.convertSmsTemplateAuditStatus("CHECKING"));
  161 + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
  162 + smsClient.convertSmsTemplateAuditStatus("SUCCESS"));
  163 + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
  164 + smsClient.convertSmsTemplateAuditStatus("FAIL"));
  165 + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("test"),
  166 + "未知审核状态(test)");
  167 + }
  168 +
  169 + @Test
  170 + public void testInvoke_throwable() {
  171 + // 准备参数
  172 + Supplier<Result<Object>> requestConsumer =
  173 + () -> new Result<>().setThrowable(new NullPointerException());
  174 + // mock 方法
  175 +
  176 + // 调用,并断言异常
  177 + assertThrows(NullPointerException.class,
  178 + () -> smsClient.invoke(requestConsumer, null));
  179 + }
  180 +
  181 + @Test
  182 + @SuppressWarnings("unchecked")
  183 + public void testInvoke_success() throws Throwable {
  184 + // 准备参数
  185 + Result<SmsSingleSend> responseResult = randomPojo(Result.class, SmsSingleSend.class, o -> o.setCode(OK));
  186 + Supplier<Result<SmsSingleSend>> requestConsumer = () -> responseResult;
  187 + Function<SmsSingleSend, SmsSendRespDTO> responseConsumer =
  188 + smsSingleSend -> new SmsSendRespDTO().setSerialNo(String.valueOf(responseResult.getData().getSid()));
  189 + // mock 方法
  190 +
  191 + // 调用
  192 + SmsCommonResult<SmsSendRespDTO> result = smsClient.invoke(requestConsumer, responseConsumer);
  193 + // 断言
  194 + assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode());
  195 + assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg());
  196 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
  197 + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
  198 + assertNull(result.getApiRequestId());
  199 + assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo());
  200 + }
  201 +
  202 +}
src/test/java/cn/iocoder/dashboard/framework/sms/core/client/impl/yunpian/YunpianSmsCodeMappingTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
  2 +
  3 +import cn.iocoder.dashboard.BaseMockitoUnitTest;
  4 +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
  5 +import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
  6 +import org.junit.jupiter.api.Test;
  7 +import org.mockito.InjectMocks;
  8 +
  9 +import static com.yunpian.sdk.constant.Code.*;
  10 +import static org.junit.jupiter.api.Assertions.assertEquals;
  11 +
  12 +/**
  13 + * {@link YunpianSmsCodeMapping} 的单元测试
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +class YunpianSmsCodeMappingTest extends BaseMockitoUnitTest {
  18 +
  19 + @InjectMocks
  20 + private YunpianSmsCodeMapping codeMapping;
  21 +
  22 + @Test
  23 + public void testApply() {
  24 + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(String.valueOf(OK)));
  25 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply(String.valueOf(ARGUMENT_MISSING)));
  26 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply(String.valueOf(BAD_ARGUMENT_FORMAT)));
  27 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply(String.valueOf(MONEY_NOT_ENOUGH)));
  28 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_FOUND)));
  29 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_VALID)));
  30 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DUP_IN_SHORT_TIME)));
  31 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(TOO_MANY_TIME_IN_5)));
  32 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DAY_LIMIT_PER_MOBILE)));
  33 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(HOUR_LIMIT_PER_MOBILE)));
  34 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply(String.valueOf(BLACK_PHONE_FILTER)));
  35 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_MATCH)));
  36 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_VALID)));
  37 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(BAD_SIGN_FORMAT)));
  38 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply(String.valueOf(BAD_API_KEY)));
  39 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply(String.valueOf(API_NOT_ALLOWED)));
  40 + assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply(String.valueOf(IP_NOT_ALLOWED)));
  41 + }
  42 +
  43 +}
src/test/java/cn/iocoder/dashboard/modules/system/service/dept/SysDeptServiceTest.java
@@ -270,4 +270,5 @@ class SysDeptServiceTest extends BaseDbUnitTest { @@ -270,4 +270,5 @@ class SysDeptServiceTest extends BaseDbUnitTest {
270 }; 270 };
271 return randomPojo(SysDeptDO.class, ArrayUtils.append(consumer, consumers)); 271 return randomPojo(SysDeptDO.class, ArrayUtils.append(consumer, consumers));
272 } 272 }
  273 +
273 } 274 }
src/test/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuServiceTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.permission;
  2 +
  3 +import cn.hutool.core.bean.BeanUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.iocoder.dashboard.BaseDbUnitTest;
  6 +import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.dashboard.modules.system.controller.permission.vo.menu.SysMenuCreateReqVO;
  8 +import cn.iocoder.dashboard.modules.system.controller.permission.vo.menu.SysMenuListReqVO;
  9 +import cn.iocoder.dashboard.modules.system.controller.permission.vo.menu.SysMenuUpdateReqVO;
  10 +import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysMenuDO;
  11 +import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysMenuMapper;
  12 +import cn.iocoder.dashboard.modules.system.enums.permission.MenuTypeEnum;
  13 +import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysMenuProducer;
  14 +import cn.iocoder.dashboard.modules.system.service.permission.impl.SysMenuServiceImpl;
  15 +import cn.iocoder.dashboard.util.AopTargetUtils;
  16 +import cn.iocoder.dashboard.util.RandomUtils;
  17 +import cn.iocoder.dashboard.util.object.ObjectUtils;
  18 +import com.google.common.collect.Multimap;
  19 +import org.junit.jupiter.api.Assertions;
  20 +import org.junit.jupiter.api.Test;
  21 +import org.springframework.boot.test.mock.mockito.MockBean;
  22 +import org.springframework.context.annotation.Import;
  23 +
  24 +import javax.annotation.Resource;
  25 +import java.util.*;
  26 +
  27 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
  28 +import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
  29 +import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
  30 +import static cn.iocoder.dashboard.util.RandomUtils.*;
  31 +import static org.junit.jupiter.api.Assertions.assertEquals;
  32 +import static org.mockito.Mockito.verify;
  33 +
  34 +@Import(SysMenuServiceImpl.class)
  35 +public class SysMenuServiceTest extends BaseDbUnitTest {
  36 +
  37 + @Resource
  38 + private SysMenuServiceImpl sysMenuService;
  39 +
  40 + @MockBean
  41 + private SysPermissionService sysPermissionService;
  42 +
  43 + @MockBean
  44 + private SysMenuProducer sysMenuProducer;
  45 +
  46 + @Resource
  47 + private SysMenuMapper menuMapper;
  48 +
  49 + @Test
  50 + public void testInitLocalCache_success() throws Exception {
  51 + SysMenuDO menuDO1 = createMenuDO(MenuTypeEnum.MENU, "xxxx", 0L);
  52 + menuMapper.insert(menuDO1);
  53 + SysMenuDO menuDO2 = createMenuDO(MenuTypeEnum.MENU, "xxxx", 0L);
  54 + menuMapper.insert(menuDO2);
  55 +
  56 + // 调用
  57 + sysMenuService.initLocalCache();
  58 +
  59 + // 获取代理对象
  60 + SysMenuServiceImpl target = (SysMenuServiceImpl) AopTargetUtils.getTarget(sysMenuService);
  61 +
  62 + Map<Long, SysMenuDO> menuCache =
  63 + (Map<Long, SysMenuDO>) BeanUtil.getFieldValue(target, "menuCache");
  64 + Assert.isTrue(menuCache.size() == 2);
  65 + assertPojoEquals(menuDO1, menuCache.get(menuDO1.getId()));
  66 + assertPojoEquals(menuDO2, menuCache.get(menuDO2.getId()));
  67 +
  68 + Multimap<String, SysMenuDO> permissionMenuCache =
  69 + (Multimap<String, SysMenuDO>) BeanUtil.getFieldValue(target, "permissionMenuCache");
  70 + Assert.isTrue(permissionMenuCache.size() == 2);
  71 + assertPojoEquals(menuDO1, permissionMenuCache.get(menuDO1.getPermission()));
  72 + assertPojoEquals(menuDO2, permissionMenuCache.get(menuDO2.getPermission()));
  73 +
  74 + Date maxUpdateTime = (Date) BeanUtil.getFieldValue(target, "maxUpdateTime");
  75 + assertEquals(ObjectUtils.max(menuDO1.getUpdateTime(), menuDO2.getUpdateTime()), maxUpdateTime);
  76 + }
  77 +
  78 + @Test
  79 + public void testCreateMenu_success() {
  80 + //构造父目录
  81 + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L);
  82 + menuMapper.insert(menuDO);
  83 + Long parentId = menuDO.getId();
  84 +
  85 + //调用
  86 + SysMenuCreateReqVO vo = randomPojo(SysMenuCreateReqVO.class, o -> {
  87 + o.setParentId(parentId);
  88 + o.setName("testSonName");
  89 + o.setType(MenuTypeEnum.MENU.getType());
  90 + o.setStatus(RandomUtils.randomCommonStatus());
  91 + });
  92 + Long menuId = sysMenuService.createMenu(vo);
  93 +
  94 + //断言
  95 + Assertions.assertNotNull(menuId);
  96 + // 校验记录的属性是否正确
  97 + SysMenuDO ret = menuMapper.selectById(menuId);
  98 + assertPojoEquals(vo, ret);
  99 + // 校验调用
  100 + verify(sysMenuProducer).sendMenuRefreshMessage();
  101 + }
  102 +
  103 + @Test
  104 + public void testUpdateMenu_success() {
  105 + //构造父子目录
  106 + SysMenuDO sonMenuDO = initParentAndSonMenuDO();
  107 + Long sonId = sonMenuDO.getId();
  108 + Long parentId = sonMenuDO.getParentId();
  109 +
  110 + //调用
  111 + SysMenuUpdateReqVO vo = RandomUtils.randomPojo(SysMenuUpdateReqVO.class, o -> {
  112 + o.setId(sonId);
  113 + o.setParentId(parentId);
  114 + o.setType(MenuTypeEnum.MENU.getType());
  115 + o.setStatus(RandomUtils.randomCommonStatus());
  116 + o.setName("pppppp"); //修改名字
  117 + });
  118 + sysMenuService.updateMenu(vo);
  119 +
  120 + //断言
  121 + // 校验记录的属性是否正确
  122 + SysMenuDO ret = menuMapper.selectById(sonId);
  123 + assertPojoEquals(vo, ret);
  124 + // 校验调用
  125 + verify(sysMenuProducer).sendMenuRefreshMessage();
  126 + }
  127 +
  128 + @Test
  129 + public void testUpdateMenu_sonIdNotExist() {
  130 + Long sonId = 99999L;
  131 + Long parentId = 10000L;
  132 +
  133 + //调用
  134 + SysMenuUpdateReqVO vo = RandomUtils.randomPojo(SysMenuUpdateReqVO.class, o -> {
  135 + o.setId(sonId);
  136 + o.setParentId(parentId);
  137 + o.setType(MenuTypeEnum.MENU.getType());
  138 + o.setStatus(RandomUtils.randomCommonStatus());
  139 + });
  140 + //断言
  141 + assertServiceException(() -> sysMenuService.updateMenu(vo), MENU_NOT_EXISTS);
  142 + }
  143 +
  144 + @Test
  145 + public void testDeleteMenu_success() {
  146 + SysMenuDO sonMenuDO = initParentAndSonMenuDO();
  147 + Long sonId = sonMenuDO.getId();
  148 +
  149 + //调用
  150 + sysMenuService.deleteMenu(sonId);
  151 +
  152 + //断言
  153 + SysMenuDO menuDO = menuMapper.selectById(sonId);
  154 + Assert.isNull(menuDO);
  155 + verify(sysPermissionService).processMenuDeleted(sonId);
  156 + verify(sysMenuProducer).sendMenuRefreshMessage();
  157 + }
  158 +
  159 + @Test
  160 + public void testDeleteMenu_menuNotExist() {
  161 + Long sonId = 99999L;
  162 +
  163 + assertServiceException(() -> sysMenuService.deleteMenu(sonId), MENU_NOT_EXISTS);
  164 + }
  165 +
  166 + @Test
  167 + public void testDeleteMenu_existChildren() {
  168 + SysMenuDO sonMenu = initParentAndSonMenuDO();
  169 + Long parentId = sonMenu.getParentId();
  170 +
  171 + assertServiceException(() -> sysMenuService.deleteMenu(parentId), MENU_EXISTS_CHILDREN);
  172 + }
  173 +
  174 + @Test
  175 + public void testGetMenus_success() {
  176 + Map<Long, SysMenuDO> idMenuMap = new HashMap<>();
  177 + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L);
  178 + menuMapper.insert(menuDO);
  179 + idMenuMap.put(menuDO.getId(), menuDO);
  180 +
  181 + SysMenuDO sonMenu = createMenuDO(MenuTypeEnum.MENU, "son", menuDO.getId());
  182 + menuMapper.insert(sonMenu);
  183 + idMenuMap.put(sonMenu.getId(), sonMenu);
  184 +
  185 + //调用
  186 + List<SysMenuDO> menuDOS = sysMenuService.getMenus();
  187 +
  188 + //断言
  189 + Assert.isTrue(menuDOS.size() == idMenuMap.size());
  190 + menuDOS.forEach(m -> assertPojoEquals(idMenuMap.get(m.getId()), m));
  191 + }
  192 +
  193 + @Test
  194 + public void testGetMenusReqVo_success() {
  195 + Map<Long, SysMenuDO> idMenuMap = new HashMap<>();
  196 + //用于验证可以模糊搜索名称包含"name",状态为1的menu
  197 + SysMenuDO menu = createMenuDO(MenuTypeEnum.MENU, "name2", 0L, 1);
  198 + menuMapper.insert(menu);
  199 + idMenuMap.put(menu.getId(), menu);
  200 +
  201 + menu = createMenuDO(MenuTypeEnum.MENU, "11name111", 0L, 1);
  202 + menuMapper.insert(menu);
  203 + idMenuMap.put(menu.getId(), menu);
  204 +
  205 + menu = createMenuDO(MenuTypeEnum.MENU, "name", 0L, 1);
  206 + menuMapper.insert(menu);
  207 + idMenuMap.put(menu.getId(), menu);
  208 +
  209 + //以下是不符合搜索条件的的menu
  210 + menu = createMenuDO(MenuTypeEnum.MENU, "xxxxxx", 0L, 1);
  211 + menuMapper.insert(menu);
  212 + menu = createMenuDO(MenuTypeEnum.MENU, "name", 0L, 2);
  213 + menuMapper.insert(menu);
  214 +
  215 + //调用
  216 + SysMenuListReqVO reqVO = new SysMenuListReqVO();
  217 + reqVO.setStatus(1);
  218 + reqVO.setName("name");
  219 + List<SysMenuDO> menuDOS = sysMenuService.getMenus(reqVO);
  220 +
  221 + //断言
  222 + Assert.isTrue(menuDOS.size() == idMenuMap.size());
  223 + menuDOS.forEach(m -> assertPojoEquals(idMenuMap.get(m.getId()), m));
  224 + }
  225 +
  226 + @Test
  227 + public void testListMenusFromCache_success() throws Exception {
  228 + Map<Long, SysMenuDO> mockCacheMap = new HashMap<>();
  229 + //获取代理对象
  230 + SysMenuServiceImpl target = (SysMenuServiceImpl) AopTargetUtils.getTarget(sysMenuService);
  231 + BeanUtil.setFieldValue(target, "menuCache", mockCacheMap);
  232 +
  233 + Map<Long, SysMenuDO> idMenuMap = new HashMap<>();
  234 + //用于验证搜索类型为MENU,状态为1的menu
  235 + SysMenuDO menuDO = createMenuDO(1L, MenuTypeEnum.MENU, "name", 0L, 1);
  236 + mockCacheMap.put(menuDO.getId(), menuDO);
  237 + idMenuMap.put(menuDO.getId(), menuDO);
  238 +
  239 + menuDO = createMenuDO(2L, MenuTypeEnum.MENU, "name", 0L, 1);
  240 + mockCacheMap.put(menuDO.getId(), menuDO);
  241 + idMenuMap.put(menuDO.getId(), menuDO);
  242 +
  243 + //以下是不符合搜索条件的menu
  244 + menuDO = createMenuDO(3L, MenuTypeEnum.BUTTON, "name", 0L, 1);
  245 + mockCacheMap.put(menuDO.getId(), menuDO);
  246 + menuDO = createMenuDO(4L, MenuTypeEnum.MENU, "name", 0L, 2);
  247 + mockCacheMap.put(menuDO.getId(), menuDO);
  248 +
  249 + List<SysMenuDO> menuDOS = sysMenuService.listMenusFromCache(Collections.singletonList(MenuTypeEnum.MENU.getType()),
  250 + Collections.singletonList(CommonStatusEnum.DISABLE.getStatus()));
  251 + Assert.isTrue(menuDOS.size() == idMenuMap.size());
  252 + menuDOS.forEach(m -> assertPojoEquals(idMenuMap.get(m.getId()), m));
  253 + }
  254 +
  255 + @Test
  256 + public void testListMenusFromCache2_success() throws Exception {
  257 + Map<Long, SysMenuDO> mockCacheMap = new HashMap<>();
  258 + //获取代理对象
  259 + SysMenuServiceImpl target = (SysMenuServiceImpl) AopTargetUtils.getTarget(sysMenuService);
  260 + BeanUtil.setFieldValue(target, "menuCache", mockCacheMap);
  261 +
  262 + Map<Long, SysMenuDO> idMenuMap = new HashMap<>();
  263 + //验证搜索id为1, 类型为MENU, 状态为1 的menu
  264 + SysMenuDO menuDO = createMenuDO(1L, MenuTypeEnum.MENU, "name", 0L, 1);
  265 + mockCacheMap.put(menuDO.getId(), menuDO);
  266 + idMenuMap.put(menuDO.getId(), menuDO);
  267 +
  268 + //以下是不符合搜索条件的menu
  269 + menuDO = createMenuDO(2L, MenuTypeEnum.MENU, "name", 0L, 1);
  270 + mockCacheMap.put(menuDO.getId(), menuDO);
  271 + menuDO = createMenuDO(3L, MenuTypeEnum.BUTTON, "name", 0L, 1);
  272 + mockCacheMap.put(menuDO.getId(), menuDO);
  273 + menuDO = createMenuDO(4L, MenuTypeEnum.MENU, "name", 0L, 2);
  274 + mockCacheMap.put(menuDO.getId(), menuDO);
  275 +
  276 + List<SysMenuDO> menuDOS = sysMenuService.listMenusFromCache(Collections.singletonList(1L),
  277 + Collections.singletonList(MenuTypeEnum.MENU.getType()), Collections.singletonList(1));
  278 + Assert.isTrue(menuDOS.size() == idMenuMap.size());
  279 + menuDOS.forEach(menu -> assertPojoEquals(idMenuMap.get(menu.getId()), menu));
  280 + }
  281 +
  282 + @Test
  283 + public void testCheckParentResource_success() {
  284 + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L);
  285 + menuMapper.insert(menuDO);
  286 + Long parentId = menuDO.getId();
  287 +
  288 + sysMenuService.checkParentResource(parentId, null);
  289 + }
  290 +
  291 + @Test
  292 + public void testCheckParentResource_canNotSetSelfToBeParent() {
  293 + assertServiceException(() -> sysMenuService.checkParentResource(1L, 1L), MENU_PARENT_ERROR);
  294 + }
  295 +
  296 + @Test
  297 + public void testCheckParentResource_parentNotExist() {
  298 + assertServiceException(() -> sysMenuService.checkParentResource(randomLongId(), null), MENU_PARENT_NOT_EXISTS);
  299 + }
  300 +
  301 + @Test
  302 + public void testCheckParentResource_parentTypeError() {
  303 + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.BUTTON, "parent", 0L);
  304 + menuMapper.insert(menuDO);
  305 + Long parentId = menuDO.getId();
  306 +
  307 + assertServiceException(() -> sysMenuService.checkParentResource(parentId, null), MENU_PARENT_NOT_DIR_OR_MENU);
  308 + }
  309 +
  310 + @Test
  311 + public void testCheckResource_success() {
  312 + SysMenuDO sonMenu = initParentAndSonMenuDO();
  313 + Long parentId = sonMenu.getParentId();
  314 +
  315 + Long otherSonMenuId = randomLongId();
  316 + String otherSonMenuName = randomString();
  317 +
  318 + sysMenuService.checkResource(parentId, otherSonMenuName, otherSonMenuId);
  319 + }
  320 +
  321 + @Test
  322 + public void testCheckResource_sonMenuNameDuplicate(){
  323 + SysMenuDO sonMenu=initParentAndSonMenuDO();
  324 + Long parentId=sonMenu.getParentId();
  325 +
  326 + Long otherSonMenuId=randomLongId();
  327 + String otherSonMenuName=sonMenu.getName(); //相同名称
  328 +
  329 + assertServiceException(() -> sysMenuService.checkResource(parentId, otherSonMenuName, otherSonMenuId), MENU_NAME_DUPLICATE);
  330 + }
  331 +
  332 + /**
  333 + * 构造父子目录,返回子目录
  334 + *
  335 + * @return
  336 + */
  337 + private SysMenuDO initParentAndSonMenuDO() {
  338 + //构造父子目录
  339 + SysMenuDO menuDO = createMenuDO(MenuTypeEnum.MENU, "parent", 0L);
  340 + menuMapper.insert(menuDO);
  341 + Long parentId = menuDO.getId();
  342 +
  343 + SysMenuDO sonMenuDO = createMenuDO(MenuTypeEnum.MENU, "testSonName", parentId);
  344 + menuMapper.insert(sonMenuDO);
  345 + return sonMenuDO;
  346 + }
  347 +
  348 + private SysMenuDO createMenuDO(MenuTypeEnum typeEnum, String menuName, Long parentId) {
  349 + return createMenuDO(typeEnum, menuName, parentId, RandomUtils.randomCommonStatus());
  350 + }
  351 +
  352 + private SysMenuDO createMenuDO(MenuTypeEnum typeEnum, String menuName, Long parentId, Integer status) {
  353 + return createMenuDO(null, typeEnum, menuName, parentId, status);
  354 + }
  355 +
  356 + private SysMenuDO createMenuDO(Long id, MenuTypeEnum typeEnum, String menuName, Long parentId, Integer status) {
  357 + return RandomUtils.randomPojo(SysMenuDO.class, o -> {
  358 + o.setId(id);
  359 + o.setParentId(parentId);
  360 + o.setType(typeEnum.getType());
  361 + o.setStatus(status);
  362 + o.setName(menuName);
  363 + });
  364 + }
  365 +
  366 +}
src/test/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleServiceTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.permission;
  2 +
  3 +import cn.hutool.core.bean.BeanUtil;
  4 +import cn.iocoder.dashboard.BaseDbUnitTest;
  5 +import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  6 +import cn.iocoder.dashboard.common.pojo.PageResult;
  7 +import cn.iocoder.dashboard.framework.security.core.enums.DataScopeEnum;
  8 +import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleCreateReqVO;
  9 +import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRolePageReqVO;
  10 +import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleUpdateReqVO;
  11 +import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO;
  12 +import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysRoleMapper;
  13 +import cn.iocoder.dashboard.modules.system.enums.permission.SysRoleTypeEnum;
  14 +import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysRoleProducer;
  15 +import cn.iocoder.dashboard.modules.system.service.permission.impl.SysRoleServiceImpl;
  16 +import cn.iocoder.dashboard.util.AopTargetUtils;
  17 +import cn.iocoder.dashboard.util.AssertUtils;
  18 +import cn.iocoder.dashboard.util.RandomUtils;
  19 +import cn.iocoder.dashboard.util.object.ObjectUtils;
  20 +import com.google.common.collect.Sets;
  21 +import org.junit.jupiter.api.Assertions;
  22 +import org.junit.jupiter.api.Test;
  23 +import org.mockito.Mockito;
  24 +import org.springframework.boot.test.mock.mockito.MockBean;
  25 +import org.springframework.context.annotation.Import;
  26 +
  27 +import javax.annotation.Resource;
  28 +import java.util.*;
  29 +import java.util.stream.Collectors;
  30 +
  31 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
  32 +import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
  33 +import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
  34 +import static cn.iocoder.dashboard.util.RandomUtils.*;
  35 +import static cn.iocoder.dashboard.util.object.ObjectUtils.max;
  36 +import static org.junit.jupiter.api.Assertions.*;
  37 +import static org.mockito.Mockito.verify;
  38 +
  39 +@Import(SysRoleServiceImpl.class)
  40 +public class SysRoleServiceTest extends BaseDbUnitTest {
  41 +
  42 + @Resource
  43 + private SysRoleServiceImpl sysRoleService;
  44 +
  45 + @Resource
  46 + private SysRoleMapper roleMapper;
  47 +
  48 + @MockBean
  49 + private SysPermissionService sysPermissionService;
  50 +
  51 + @MockBean
  52 + private SysRoleProducer sysRoleProducer;
  53 +
  54 + @Test
  55 + public void testInitLocalCache_success() throws Exception {
  56 + SysRoleDO roleDO1 = createRoleDO("role1", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL);
  57 + roleMapper.insert(roleDO1);
  58 + SysRoleDO roleDO2 = createRoleDO("role2", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL);
  59 + roleMapper.insert(roleDO2);
  60 +
  61 + //调用
  62 + sysRoleService.initLocalCache();
  63 +
  64 + //断言
  65 + //获取代理对象
  66 + SysRoleServiceImpl target = (SysRoleServiceImpl) AopTargetUtils.getTarget(sysRoleService);
  67 +
  68 + Map<Long, SysRoleDO> roleCache = (Map<Long, SysRoleDO>) BeanUtil.getFieldValue(target, "roleCache");
  69 + assertPojoEquals(roleDO1, roleCache.get(roleDO1.getId()));
  70 + assertPojoEquals(roleDO2, roleCache.get(roleDO2.getId()));
  71 +
  72 + Date maxUpdateTime = (Date) BeanUtil.getFieldValue(target, "maxUpdateTime");
  73 + assertEquals(max(roleDO1.getUpdateTime(), roleDO2.getUpdateTime()), maxUpdateTime);
  74 + }
  75 +
  76 + @Test
  77 + public void testCreateRole_success() {
  78 + SysRoleCreateReqVO reqVO = randomPojo(SysRoleCreateReqVO.class, o -> {
  79 + o.setCode("role_code");
  80 + o.setName("role_name");
  81 + o.setRemark("remark");
  82 + o.setType(SysRoleTypeEnum.CUSTOM.getType());
  83 + o.setSort(1);
  84 + });
  85 + Long roleId = sysRoleService.createRole(reqVO);
  86 +
  87 + //断言
  88 + assertNotNull(roleId);
  89 + SysRoleDO roleDO = roleMapper.selectById(roleId);
  90 + assertPojoEquals(reqVO, roleDO);
  91 +
  92 + verify(sysRoleProducer).sendRoleRefreshMessage();
  93 + }
  94 +
  95 + @Test
  96 + public void testUpdateRole_success() {
  97 + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL);
  98 + roleMapper.insert(roleDO);
  99 + Long roleId = roleDO.getId();
  100 +
  101 + //调用
  102 + SysRoleUpdateReqVO reqVO = randomPojo(SysRoleUpdateReqVO.class, o -> {
  103 + o.setId(roleId);
  104 + o.setCode("role_code");
  105 + o.setName("update_name");
  106 + o.setType(SysRoleTypeEnum.SYSTEM.getType());
  107 + o.setSort(999);
  108 + });
  109 + sysRoleService.updateRole(reqVO);
  110 +
  111 + //断言
  112 + SysRoleDO newRoleDO = roleMapper.selectById(roleId);
  113 + assertPojoEquals(reqVO, newRoleDO);
  114 +
  115 + verify(sysRoleProducer).sendRoleRefreshMessage();
  116 + }
  117 +
  118 + @Test
  119 + public void testUpdateRoleStatus_success() {
  120 + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, CommonStatusEnum.ENABLE.getStatus());
  121 + roleMapper.insert(roleDO);
  122 + Long roleId = roleDO.getId();
  123 +
  124 + //调用
  125 + sysRoleService.updateRoleStatus(roleId, CommonStatusEnum.DISABLE.getStatus());
  126 +
  127 + //断言
  128 + SysRoleDO newRoleDO = roleMapper.selectById(roleId);
  129 + assertEquals(CommonStatusEnum.DISABLE.getStatus(), newRoleDO.getStatus());
  130 +
  131 + verify(sysRoleProducer).sendRoleRefreshMessage();
  132 + }
  133 +
  134 + @Test
  135 + public void testUpdateRoleDataScope_success() {
  136 + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL);
  137 + roleMapper.insert(roleDO);
  138 + Long roleId = roleDO.getId();
  139 +
  140 + //调用
  141 + Set<Long> deptIdSet = Arrays.asList(1L, 2L, 3L, 4L, 5L).stream().collect(Collectors.toSet());
  142 + sysRoleService.updateRoleDataScope(roleId, DataScopeEnum.DEPT_CUSTOM.getScore(), deptIdSet);
  143 +
  144 + //断言
  145 + SysRoleDO newRoleDO = roleMapper.selectById(roleId);
  146 + assertEquals(DataScopeEnum.DEPT_CUSTOM.getScore(), newRoleDO.getDataScope());
  147 +
  148 + Set<Long> newDeptIdSet = newRoleDO.getDataScopeDeptIds();
  149 + assertTrue(deptIdSet.size() == newDeptIdSet.size());
  150 + deptIdSet.stream().forEach(d -> assertTrue(newDeptIdSet.contains(d)));
  151 +
  152 + verify(sysRoleProducer).sendRoleRefreshMessage();
  153 + }
  154 +
  155 + @Test
  156 + public void testDeleteRole_success() {
  157 + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL);
  158 + roleMapper.insert(roleDO);
  159 + Long roleId = roleDO.getId();
  160 +
  161 + //调用
  162 + sysRoleService.deleteRole(roleId);
  163 +
  164 + //断言
  165 + SysRoleDO newRoleDO = roleMapper.selectById(roleId);
  166 + assertNull(newRoleDO);
  167 +
  168 + verify(sysRoleProducer).sendRoleRefreshMessage();
  169 + }
  170 +
  171 + @Test
  172 + public void testGetRoles_success() {
  173 + Map<Long, SysRoleDO> idRoleMap = new HashMap<>();
  174 + // 验证查询状态为1的角色
  175 + SysRoleDO roleDO1 = createRoleDO("role1", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1);
  176 + roleMapper.insert(roleDO1);
  177 + idRoleMap.put(roleDO1.getId(), roleDO1);
  178 +
  179 + SysRoleDO roleDO2 = createRoleDO("role2", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1);
  180 + roleMapper.insert(roleDO2);
  181 + idRoleMap.put(roleDO2.getId(), roleDO2);
  182 +
  183 + // 以下是排除的角色
  184 + SysRoleDO roleDO3 = createRoleDO("role3", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 2);
  185 + roleMapper.insert(roleDO3);
  186 +
  187 + //调用
  188 + List<SysRoleDO> roles = sysRoleService.getRoles(Arrays.asList(1));
  189 +
  190 + //断言
  191 + assertEquals(2, roles.size());
  192 + roles.stream().forEach(r -> assertPojoEquals(idRoleMap.get(r.getId()), r));
  193 +
  194 + }
  195 +
  196 + @Test
  197 + public void testGetRolePage_success() {
  198 + Map<Long, SysRoleDO> idRoleMap = new HashMap<>();
  199 + // 验证名称包含"role", 状态为1,code为"code"的角色
  200 + // 第一页
  201 + SysRoleDO roleDO = createRoleDO("role1", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1, "code");
  202 + roleMapper.insert(roleDO);
  203 + idRoleMap.put(roleDO.getId(), roleDO);
  204 + // 第二页
  205 + roleDO = createRoleDO("role2", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1, "code");
  206 + roleMapper.insert(roleDO);
  207 +
  208 + // 以下是排除的角色
  209 + roleDO = createRoleDO("role3", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 2, "code");
  210 + roleMapper.insert(roleDO);
  211 + roleDO = createRoleDO("role4", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL, 1, "xxxxx");
  212 + roleMapper.insert(roleDO);
  213 +
  214 + //调用
  215 + SysRolePageReqVO reqVO = randomPojo(SysRolePageReqVO.class, o -> {
  216 + o.setName("role");
  217 + o.setCode("code");
  218 + o.setStatus(1);
  219 + o.setPageNo(1);
  220 + o.setPageSize(1);
  221 + o.setBeginTime(null);
  222 + o.setEndTime(null);
  223 + });
  224 + PageResult<SysRoleDO> result = sysRoleService.getRolePage(reqVO);
  225 + assertEquals(2, result.getTotal());
  226 + result.getList().stream().forEach(r -> assertPojoEquals(idRoleMap.get(r.getId()), r));
  227 + }
  228 +
  229 + @Test
  230 + public void testCheckDuplicateRole_success() {
  231 + sysRoleService.checkDuplicateRole(randomString(), randomString(), null);
  232 + }
  233 +
  234 + @Test
  235 + public void testCheckDuplicateRole_nameDuplicate() {
  236 + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL);
  237 + roleMapper.insert(roleDO);
  238 +
  239 + String duplicateName = "role_name";
  240 +
  241 + assertServiceException(() -> sysRoleService.checkDuplicateRole(duplicateName, randomString(), null), ROLE_NAME_DUPLICATE, duplicateName);
  242 + }
  243 +
  244 + @Test
  245 + public void testCheckDuplicateRole_codeDuplicate() {
  246 + SysRoleDO roleDO = randomPojo(SysRoleDO.class, o -> {
  247 + o.setName("role_999");
  248 + o.setCode("code");
  249 + o.setType(SysRoleTypeEnum.CUSTOM.getType());
  250 + o.setStatus(1);
  251 + o.setDataScope(DataScopeEnum.ALL.getScore());
  252 + });
  253 + roleMapper.insert(roleDO);
  254 +
  255 + String randomName = randomString();
  256 + String duplicateCode = "code";
  257 +
  258 + assertServiceException(() -> sysRoleService.checkDuplicateRole(randomName, duplicateCode, null), ROLE_CODE_DUPLICATE, duplicateCode);
  259 + }
  260 +
  261 + @Test
  262 + public void testCheckUpdateRole_success() {
  263 + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.CUSTOM, DataScopeEnum.ALL);
  264 + roleMapper.insert(roleDO);
  265 + Long roleId = roleDO.getId();
  266 +
  267 + sysRoleService.checkUpdateRole(roleId);
  268 + }
  269 +
  270 + @Test
  271 + public void testCheckUpdateRole_roleIdNotExist() {
  272 + assertServiceException(() -> sysRoleService.checkUpdateRole(randomLongId()), ROLE_NOT_EXISTS);
  273 + }
  274 +
  275 + @Test
  276 + public void testCheckUpdateRole_systemRoleCanNotBeUpdate() {
  277 + SysRoleDO roleDO = createRoleDO("role_name", SysRoleTypeEnum.SYSTEM, DataScopeEnum.ALL);
  278 + roleMapper.insert(roleDO);
  279 + Long roleId = roleDO.getId();
  280 +
  281 + assertServiceException(() -> sysRoleService.checkUpdateRole(roleId), ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE);
  282 + }
  283 +
  284 + private SysRoleDO createRoleDO(String name, SysRoleTypeEnum typeEnum, DataScopeEnum scopeEnum, Integer status) {
  285 + return createRoleDO( name, typeEnum, scopeEnum, status, randomString());
  286 + }
  287 +
  288 + private SysRoleDO createRoleDO(String name, SysRoleTypeEnum typeEnum, DataScopeEnum scopeEnum, Integer status, String code) {
  289 + return createRoleDO(null, name, typeEnum, scopeEnum, status, code);
  290 + }
  291 +
  292 + private SysRoleDO createRoleDO(String name, SysRoleTypeEnum typeEnum, DataScopeEnum scopeEnum) {
  293 + return createRoleDO(null, name, typeEnum, scopeEnum, randomCommonStatus(), randomString());
  294 + }
  295 +
  296 + private SysRoleDO createRoleDO(Long id, String name, SysRoleTypeEnum typeEnum, DataScopeEnum scopeEnum, Integer status, String code) {
  297 + SysRoleDO roleDO = randomPojo(SysRoleDO.class, o -> {
  298 + o.setId(id);
  299 + o.setName(name);
  300 + o.setType(typeEnum.getType());
  301 + o.setStatus(status);
  302 + o.setDataScope(scopeEnum.getScore());
  303 + o.setCode(code);
  304 + });
  305 + return roleDO;
  306 + }
  307 +
  308 +}
src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsChannelServiceTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms;
  2 +
  3 +import cn.hutool.core.bean.BeanUtil;
  4 +import cn.iocoder.dashboard.BaseDbUnitTest;
  5 +import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  6 +import cn.iocoder.dashboard.common.pojo.PageResult;
  7 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
  8 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelCreateReqVO;
  9 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelPageReqVO;
  10 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.channel.SysSmsChannelUpdateReqVO;
  11 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
  12 +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsChannelMapper;
  13 +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
  14 +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsChannelServiceImpl;
  15 +import cn.iocoder.dashboard.util.collection.ArrayUtils;
  16 +import cn.iocoder.dashboard.util.object.ObjectUtils;
  17 +import org.junit.jupiter.api.Test;
  18 +import org.springframework.boot.test.mock.mockito.MockBean;
  19 +import org.springframework.context.annotation.Import;
  20 +
  21 +import javax.annotation.Resource;
  22 +import java.util.Date;
  23 +import java.util.function.Consumer;
  24 +
  25 +import static cn.hutool.core.util.RandomUtil.randomEle;
  26 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
  27 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
  28 +import static cn.iocoder.dashboard.util.AssertUtils.*;
  29 +import static cn.iocoder.dashboard.util.RandomUtils.*;
  30 +import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
  31 +import static cn.iocoder.dashboard.util.object.ObjectUtils.max;
  32 +import static org.junit.jupiter.api.Assertions.*;
  33 +import static org.mockito.ArgumentMatchers.eq;
  34 +import static org.mockito.Mockito.*;
  35 +
  36 +/**
  37 +* {@link SysSmsChannelServiceImpl} 的单元测试类
  38 +*
  39 +* @author 芋道源码
  40 +*/
  41 +@Import(SysSmsChannelServiceImpl.class)
  42 +public class SysSmsChannelServiceTest extends BaseDbUnitTest {
  43 +
  44 + @Resource
  45 + private SysSmsChannelServiceImpl smsChannelService;
  46 +
  47 + @Resource
  48 + private SysSmsChannelMapper smsChannelMapper;
  49 +
  50 + @MockBean
  51 + private SmsClientFactory smsClientFactory;
  52 + @MockBean
  53 + private SysSmsTemplateService smsTemplateService;
  54 + @MockBean
  55 + private SysSmsProducer smsProducer;
  56 +
  57 + @Test
  58 + public void testInitLocalCache_success() {
  59 + // mock 数据s
  60 + SysSmsChannelDO smsChannelDO01 = randomSmsChannelDO();
  61 + smsChannelMapper.insert(smsChannelDO01);
  62 + SysSmsChannelDO smsChannelDO02 = randomSmsChannelDO();
  63 + smsChannelMapper.insert(smsChannelDO02);
  64 +
  65 + // 调用
  66 + smsChannelService.initSmsClients();
  67 + // 校验 maxUpdateTime 属性
  68 + Date maxUpdateTime = (Date) BeanUtil.getFieldValue(smsChannelService, "maxUpdateTime");
  69 + assertEquals(max(smsChannelDO01.getUpdateTime(), smsChannelDO02.getUpdateTime()), maxUpdateTime);
  70 + // 校验调用
  71 + verify(smsClientFactory, times(1)).createOrUpdateSmsClient(
  72 + argThat(properties -> isPojoEquals(smsChannelDO01, properties)));
  73 + verify(smsClientFactory, times(1)).createOrUpdateSmsClient(
  74 + argThat(properties -> isPojoEquals(smsChannelDO02, properties)));
  75 + }
  76 +
  77 + @Test
  78 + public void testCreateSmsChannel_success() {
  79 + // 准备参数
  80 + SysSmsChannelCreateReqVO reqVO = randomPojo(SysSmsChannelCreateReqVO.class, o -> o.setStatus(randomCommonStatus()));
  81 +
  82 + // 调用
  83 + Long smsChannelId = smsChannelService.createSmsChannel(reqVO);
  84 + // 断言
  85 + assertNotNull(smsChannelId);
  86 + // 校验记录的属性是否正确
  87 + SysSmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId);
  88 + assertPojoEquals(reqVO, smsChannel);
  89 + // 校验调用
  90 + verify(smsProducer, times(1)).sendSmsChannelRefreshMessage();
  91 + }
  92 +
  93 + @Test
  94 + public void testUpdateSmsChannel_success() {
  95 + // mock 数据
  96 + SysSmsChannelDO dbSmsChannel = randomSmsChannelDO();
  97 + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据
  98 + // 准备参数
  99 + SysSmsChannelUpdateReqVO reqVO = randomPojo(SysSmsChannelUpdateReqVO.class, o -> {
  100 + o.setId(dbSmsChannel.getId()); // 设置更新的 ID
  101 + o.setStatus(randomCommonStatus());
  102 + o.setCallbackUrl(randomString());
  103 + });
  104 +
  105 + // 调用
  106 + smsChannelService.updateSmsChannel(reqVO);
  107 + // 校验是否更新正确
  108 + SysSmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的
  109 + assertPojoEquals(reqVO, smsChannel);
  110 + // 校验调用
  111 + verify(smsProducer, times(1)).sendSmsChannelRefreshMessage();
  112 + }
  113 +
  114 + @Test
  115 + public void testUpdateSmsChannel_notExists() {
  116 + // 准备参数
  117 + SysSmsChannelUpdateReqVO reqVO = randomPojo(SysSmsChannelUpdateReqVO.class);
  118 +
  119 + // 调用, 并断言异常
  120 + assertServiceException(() -> smsChannelService.updateSmsChannel(reqVO), SMS_CHANNEL_NOT_EXISTS);
  121 + }
  122 +
  123 + @Test
  124 + public void testDeleteSmsChannel_success() {
  125 + // mock 数据
  126 + SysSmsChannelDO dbSmsChannel = randomSmsChannelDO();
  127 + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据
  128 + // 准备参数
  129 + Long id = dbSmsChannel.getId();
  130 +
  131 + // 调用
  132 + smsChannelService.deleteSmsChannel(id);
  133 + // 校验数据不存在了
  134 + assertNull(smsChannelMapper.selectById(id));
  135 + // 校验调用
  136 + verify(smsProducer, times(1)).sendSmsChannelRefreshMessage();
  137 + }
  138 +
  139 + @Test
  140 + public void testDeleteSmsChannel_notExists() {
  141 + // 准备参数
  142 + Long id = randomLongId();
  143 +
  144 + // 调用, 并断言异常
  145 + assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_NOT_EXISTS);
  146 + }
  147 +
  148 + @Test
  149 + public void testDeleteSmsChannel_hasChildren() {
  150 + // mock 数据
  151 + SysSmsChannelDO dbSmsChannel = randomSmsChannelDO();
  152 + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据
  153 + // 准备参数
  154 + Long id = dbSmsChannel.getId();
  155 + // mock 方法
  156 + when(smsTemplateService.countByChannelId(eq(id))).thenReturn(10);
  157 +
  158 + // 调用, 并断言异常
  159 + assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_HAS_CHILDREN);
  160 + }
  161 +
  162 + @Test
  163 + public void testGetSmsChannelPage() {
  164 + // mock 数据
  165 + SysSmsChannelDO dbSmsChannel = randomPojo(SysSmsChannelDO.class, o -> { // 等会查询到
  166 + o.setSignature("芋道源码");
  167 + o.setStatus(CommonStatusEnum.ENABLE.getStatus());
  168 + o.setCreateTime(buildTime(2020, 12, 12));
  169 + });
  170 + smsChannelMapper.insert(dbSmsChannel);
  171 + // 测试 signature 不匹配
  172 + smsChannelMapper.insert(ObjectUtils.clone(dbSmsChannel, o -> o.setSignature("源码")));
  173 + // 测试 status 不匹配
  174 + smsChannelMapper.insert(ObjectUtils.clone(dbSmsChannel, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
  175 + // 测试 createTime 不匹配
  176 + smsChannelMapper.insert(ObjectUtils.clone(dbSmsChannel, o -> o.setCreateTime(buildTime(2020, 11, 11))));
  177 + // 准备参数
  178 + SysSmsChannelPageReqVO reqVO = new SysSmsChannelPageReqVO();
  179 + reqVO.setSignature("芋道");
  180 + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
  181 + reqVO.setBeginCreateTime(buildTime(2020, 12, 1));
  182 + reqVO.setEndCreateTime(buildTime(2020, 12, 24));
  183 +
  184 + // 调用
  185 + PageResult<SysSmsChannelDO> pageResult = smsChannelService.getSmsChannelPage(reqVO);
  186 + // 断言
  187 + assertEquals(1, pageResult.getTotal());
  188 + assertEquals(1, pageResult.getList().size());
  189 + assertPojoEquals(dbSmsChannel, pageResult.getList().get(0));
  190 + }
  191 +
  192 + // ========== 随机对象 ==========
  193 +
  194 + @SafeVarargs
  195 + private static SysSmsChannelDO randomSmsChannelDO(Consumer<SysSmsChannelDO>... consumers) {
  196 + Consumer<SysSmsChannelDO> consumer = (o) -> {
  197 + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
  198 + };
  199 + return randomPojo(SysSmsChannelDO.class, ArrayUtils.append(consumer, consumers));
  200 + }
  201 +
  202 +}
src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsLogServiceTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.iocoder.dashboard.BaseDbUnitTest;
  5 +import cn.iocoder.dashboard.common.enums.UserTypeEnum;
  6 +import cn.iocoder.dashboard.common.pojo.CommonResult;
  7 +import cn.iocoder.dashboard.common.pojo.PageResult;
  8 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogExportReqVO;
  9 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.log.SysSmsLogPageReqVO;
  10 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsLogDO;
  11 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  12 +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsLogMapper;
  13 +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsReceiveStatusEnum;
  14 +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum;
  15 +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsTemplateTypeEnum;
  16 +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsLogServiceImpl;
  17 +import cn.iocoder.dashboard.util.collection.ArrayUtils;
  18 +import cn.iocoder.dashboard.util.object.ObjectUtils;
  19 +import org.junit.jupiter.api.Test;
  20 +import org.springframework.context.annotation.Import;
  21 +
  22 +import javax.annotation.Resource;
  23 +import java.util.Date;
  24 +import java.util.List;
  25 +import java.util.Map;
  26 +import java.util.function.Consumer;
  27 +
  28 +import static cn.hutool.core.util.RandomUtil.randomBoolean;
  29 +import static cn.hutool.core.util.RandomUtil.randomEle;
  30 +import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
  31 +import static cn.iocoder.dashboard.util.RandomUtils.*;
  32 +import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
  33 +import static org.junit.jupiter.api.Assertions.*;
  34 +
  35 +/**
  36 +* {@link SysSmsLogServiceImpl} 的单元测试类
  37 +*
  38 +* @author 芋道源码
  39 +*/
  40 +@Import(SysSmsLogServiceImpl.class)
  41 +public class SysSmsLogServiceTest extends BaseDbUnitTest {
  42 +
  43 + @Resource
  44 + private SysSmsLogServiceImpl smsLogService;
  45 +
  46 + @Resource
  47 + private SysSmsLogMapper smsLogMapper;
  48 +
  49 + @Test
  50 + public void testCreateSmsLog() {
  51 + // 准备参数
  52 + String mobile = randomString();
  53 + Long userId = randomLongId();
  54 + Integer userType = randomEle(UserTypeEnum.values()).getValue();
  55 + Boolean isSend = randomBoolean();
  56 + SysSmsTemplateDO templateDO = randomPojo(SysSmsTemplateDO.class,
  57 + o -> o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType()));
  58 + String templateContent = randomString();
  59 + Map<String, Object> templateParams = randomTemplateParams();
  60 + // mock 方法
  61 +
  62 + // 调用
  63 + Long logId = smsLogService.createSmsLog(mobile, userId, userType, isSend,
  64 + templateDO, templateContent, templateParams);
  65 + // 断言
  66 + SysSmsLogDO logDO = smsLogMapper.selectById(logId);
  67 + assertEquals(isSend ? SysSmsSendStatusEnum.INIT.getStatus() : SysSmsSendStatusEnum.IGNORE.getStatus(),
  68 + logDO.getSendStatus());
  69 + assertEquals(mobile, logDO.getMobile());
  70 + assertEquals(userType, logDO.getUserType());
  71 + assertEquals(userId, logDO.getUserId());
  72 + assertEquals(templateDO.getId(), logDO.getTemplateId());
  73 + assertEquals(templateDO.getCode(), logDO.getTemplateCode());
  74 + assertEquals(templateDO.getType(), logDO.getTemplateType());
  75 + assertEquals(templateDO.getChannelId(), logDO.getChannelId());
  76 + assertEquals(templateDO.getChannelCode(), logDO.getChannelCode());
  77 + assertEquals(templateContent, logDO.getTemplateContent());
  78 + assertEquals(templateParams, logDO.getTemplateParams());
  79 + assertEquals(SysSmsReceiveStatusEnum.INIT.getStatus(), logDO.getReceiveStatus());
  80 + }
  81 +
  82 + @Test
  83 + public void testUpdateSmsSendResult() {
  84 + // mock 数据
  85 + SysSmsLogDO dbSmsLog = randomSmsLogDO(
  86 + o -> o.setSendStatus(SysSmsSendStatusEnum.IGNORE.getStatus()));
  87 + smsLogMapper.insert(dbSmsLog);
  88 + // 准备参数
  89 + Long id = dbSmsLog.getId();
  90 + Integer sendCode = randomInteger();
  91 + String sendMsg = randomString();
  92 + String apiSendCode = randomString();
  93 + String apiSendMsg = randomString();
  94 + String apiRequestId = randomString();
  95 + String apiSerialNo = randomString();
  96 +
  97 + // 调用
  98 + smsLogService.updateSmsSendResult(id, sendCode, sendMsg,
  99 + apiSendCode, apiSendMsg, apiRequestId, apiSerialNo);
  100 + // 断言
  101 + dbSmsLog = smsLogMapper.selectById(id);
  102 + assertEquals(CommonResult.isSuccess(sendCode) ? SysSmsSendStatusEnum.SUCCESS.getStatus()
  103 + : SysSmsSendStatusEnum.FAILURE.getStatus(), dbSmsLog.getSendStatus());
  104 + assertNotNull(dbSmsLog.getSendTime());
  105 + assertEquals(sendMsg, dbSmsLog.getSendMsg());
  106 + assertEquals(apiSendCode, dbSmsLog.getApiSendCode());
  107 + assertEquals(apiSendMsg, dbSmsLog.getApiSendMsg());
  108 + assertEquals(apiRequestId, dbSmsLog.getApiRequestId());
  109 + assertEquals(apiSerialNo, dbSmsLog.getApiSerialNo());
  110 + }
  111 +
  112 + @Test
  113 + public void testUpdateSmsReceiveResult() {
  114 + // mock 数据
  115 + SysSmsLogDO dbSmsLog = randomSmsLogDO(
  116 + o -> o.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus()));
  117 + smsLogMapper.insert(dbSmsLog);
  118 + // 准备参数
  119 + Long id = dbSmsLog.getId();
  120 + Boolean success = randomBoolean();
  121 + Date receiveTime = randomDate();
  122 + String apiReceiveCode = randomString();
  123 + String apiReceiveMsg = randomString();
  124 +
  125 + // 调用
  126 + smsLogService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg);
  127 + // 断言
  128 + dbSmsLog = smsLogMapper.selectById(id);
  129 + assertEquals(success ? SysSmsReceiveStatusEnum.SUCCESS.getStatus()
  130 + : SysSmsReceiveStatusEnum.FAILURE.getStatus(), dbSmsLog.getReceiveStatus());
  131 + assertEquals(receiveTime, dbSmsLog.getReceiveTime());
  132 + assertEquals(apiReceiveCode, dbSmsLog.getApiReceiveCode());
  133 + assertEquals(apiReceiveMsg, dbSmsLog.getApiReceiveMsg());
  134 + }
  135 +
  136 + @Test
  137 + public void testGetSmsLogPage() {
  138 + // mock 数据
  139 + SysSmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到
  140 + o.setChannelId(1L);
  141 + o.setTemplateId(10L);
  142 + o.setMobile("15601691300");
  143 + o.setSendStatus(SysSmsSendStatusEnum.INIT.getStatus());
  144 + o.setSendTime(buildTime(2020, 11, 11));
  145 + o.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus());
  146 + o.setReceiveTime(buildTime(2021, 11, 11));
  147 + });
  148 + smsLogMapper.insert(dbSmsLog);
  149 + // 测试 channelId 不匹配
  150 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setChannelId(2L)));
  151 + // 测试 templateId 不匹配
  152 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setTemplateId(20L)));
  153 + // 测试 mobile 不匹配
  154 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setMobile("18818260999")));
  155 + // 测试 sendStatus 不匹配
  156 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setSendStatus(SysSmsSendStatusEnum.IGNORE.getStatus())));
  157 + // 测试 sendTime 不匹配
  158 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12))));
  159 + // 测试 receiveStatus 不匹配
  160 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setReceiveStatus(SysSmsReceiveStatusEnum.SUCCESS.getStatus())));
  161 + // 测试 receiveTime 不匹配
  162 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12))));
  163 + // 准备参数
  164 + SysSmsLogPageReqVO reqVO = new SysSmsLogPageReqVO();
  165 + reqVO.setChannelId(1L);
  166 + reqVO.setTemplateId(10L);
  167 + reqVO.setMobile("156");
  168 + reqVO.setSendStatus(SysSmsSendStatusEnum.INIT.getStatus());
  169 + reqVO.setBeginSendTime(buildTime(2020, 11, 1));
  170 + reqVO.setEndSendTime(buildTime(2020, 11, 30));
  171 + reqVO.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus());
  172 + reqVO.setBeginReceiveTime(buildTime(2021, 11, 1));
  173 + reqVO.setEndReceiveTime(buildTime(2021, 11, 30));
  174 +
  175 + // 调用
  176 + PageResult<SysSmsLogDO> pageResult = smsLogService.getSmsLogPage(reqVO);
  177 + // 断言
  178 + assertEquals(1, pageResult.getTotal());
  179 + assertEquals(1, pageResult.getList().size());
  180 + assertPojoEquals(dbSmsLog, pageResult.getList().get(0));
  181 + }
  182 +
  183 + @Test
  184 + public void testGetSmsLogList() {
  185 + // mock 数据
  186 + SysSmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到
  187 + o.setChannelId(1L);
  188 + o.setTemplateId(10L);
  189 + o.setMobile("15601691300");
  190 + o.setSendStatus(SysSmsSendStatusEnum.INIT.getStatus());
  191 + o.setSendTime(buildTime(2020, 11, 11));
  192 + o.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus());
  193 + o.setReceiveTime(buildTime(2021, 11, 11));
  194 + });
  195 + smsLogMapper.insert(dbSmsLog);
  196 + // 测试 channelId 不匹配
  197 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setChannelId(2L)));
  198 + // 测试 templateId 不匹配
  199 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setTemplateId(20L)));
  200 + // 测试 mobile 不匹配
  201 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setMobile("18818260999")));
  202 + // 测试 sendStatus 不匹配
  203 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setSendStatus(SysSmsSendStatusEnum.IGNORE.getStatus())));
  204 + // 测试 sendTime 不匹配
  205 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12))));
  206 + // 测试 receiveStatus 不匹配
  207 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setReceiveStatus(SysSmsReceiveStatusEnum.SUCCESS.getStatus())));
  208 + // 测试 receiveTime 不匹配
  209 + smsLogMapper.insert(ObjectUtils.clone(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12))));
  210 + // 准备参数
  211 + SysSmsLogExportReqVO reqVO = new SysSmsLogExportReqVO();
  212 + reqVO.setChannelId(1L);
  213 + reqVO.setTemplateId(10L);
  214 + reqVO.setMobile("156");
  215 + reqVO.setSendStatus(SysSmsSendStatusEnum.INIT.getStatus());
  216 + reqVO.setBeginSendTime(buildTime(2020, 11, 1));
  217 + reqVO.setEndSendTime(buildTime(2020, 11, 30));
  218 + reqVO.setReceiveStatus(SysSmsReceiveStatusEnum.INIT.getStatus());
  219 + reqVO.setBeginReceiveTime(buildTime(2021, 11, 1));
  220 + reqVO.setEndReceiveTime(buildTime(2021, 11, 30));
  221 +
  222 + // 调用
  223 + List<SysSmsLogDO> list = smsLogService.getSmsLogList(reqVO);
  224 + // 断言
  225 + assertEquals(1, list.size());
  226 + assertPojoEquals(dbSmsLog, list.get(0));
  227 + }
  228 +
  229 + // ========== 随机对象 ==========
  230 +
  231 + @SafeVarargs
  232 + private static SysSmsLogDO randomSmsLogDO(Consumer<SysSmsLogDO>... consumers) {
  233 + Consumer<SysSmsLogDO> consumer = (o) -> {
  234 + o.setTemplateParams(randomTemplateParams());
  235 + o.setTemplateType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 templateType 的范围
  236 + o.setUserType(randomEle(UserTypeEnum.values()).getValue()); // 保证 userType 的范围
  237 + o.setSendStatus(randomEle(SysSmsSendStatusEnum.values()).getStatus()); // 保证 sendStatus 的范围
  238 + o.setReceiveStatus(randomEle(SysSmsReceiveStatusEnum.values()).getStatus()); // 保证 receiveStatus 的范围
  239 + };
  240 + return randomPojo(SysSmsLogDO.class, ArrayUtils.append(consumer, consumers));
  241 + }
  242 +
  243 +
  244 + private static Map<String, Object> randomTemplateParams() {
  245 + return MapUtil.<String, Object>builder().put(randomString(), randomString())
  246 + .put(randomString(), randomString()).build();
  247 + }
  248 +}
src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsServiceTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.iocoder.dashboard.BaseMockitoUnitTest;
  5 +import cn.iocoder.dashboard.common.core.KeyValue;
  6 +import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.dashboard.common.enums.UserTypeEnum;
  8 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
  9 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
  10 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  11 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
  12 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
  13 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  14 +import cn.iocoder.dashboard.modules.system.mq.message.sms.SysSmsSendMessage;
  15 +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
  16 +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsServiceImpl;
  17 +import org.assertj.core.util.Lists;
  18 +import org.junit.jupiter.api.Test;
  19 +import org.mockito.InjectMocks;
  20 +import org.mockito.Mock;
  21 +
  22 +import java.util.HashMap;
  23 +import java.util.List;
  24 +import java.util.Map;
  25 +
  26 +import static cn.hutool.core.util.RandomUtil.randomEle;
  27 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
  28 +import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
  29 +import static cn.iocoder.dashboard.util.RandomUtils.*;
  30 +import static org.junit.jupiter.api.Assertions.assertEquals;
  31 +import static org.mockito.ArgumentMatchers.eq;
  32 +import static org.mockito.Mockito.*;
  33 +
  34 +/**
  35 + * {@link SysSmsServiceImpl} 的单元测试类
  36 + *
  37 + * @author 芋道源码
  38 + */
  39 +public class SysSmsServiceTest extends BaseMockitoUnitTest {
  40 +
  41 + @InjectMocks
  42 + private SysSmsServiceImpl smsService;
  43 +
  44 + @Mock
  45 + private SysSmsTemplateService smsTemplateService;
  46 + @Mock
  47 + private SysSmsLogService smsLogService;
  48 + @Mock
  49 + private SysSmsProducer smsProducer;
  50 + @Mock
  51 + private SmsClientFactory smsClientFactory;
  52 +
  53 + /**
  54 + * 发送成功,当短信模板开启时
  55 + */
  56 + @Test
  57 + public void testSendSingleSms_successWhenSmsTemplateEnable() {
  58 + // 准备参数
  59 + String mobile = randomString();
  60 + Long userId = randomLongId();
  61 + Integer userType = randomEle(UserTypeEnum.values()).getValue();
  62 + String templateCode = randomString();
  63 + Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
  64 + .put("op", "login").build();
  65 + // mock SmsTemplateService 的方法
  66 + SysSmsTemplateDO template = randomPojo(SysSmsTemplateDO.class, o -> {
  67 + o.setStatus(CommonStatusEnum.ENABLE.getStatus());
  68 + o.setContent("验证码为{code}, 操作为{op}");
  69 + o.setParams(Lists.newArrayList("code", "op"));
  70 + });
  71 + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
  72 + String content = randomString();
  73 + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
  74 + .thenReturn(content);
  75 + // mock SmsLogService 的方法
  76 + Long smsLogId = randomLongId();
  77 + when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.TRUE), eq(template),
  78 + eq(content), eq(templateParams))).thenReturn(smsLogId);
  79 +
  80 + // 调用
  81 + Long resultSmsLogId = smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams);
  82 + // 断言
  83 + assertEquals(smsLogId, resultSmsLogId);
  84 + // 断言调用
  85 + verify(smsProducer, times(1)).sendSmsSendMessage(eq(smsLogId), eq(mobile),
  86 + eq(template.getChannelId()), eq(template.getApiTemplateId()),
  87 + eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
  88 + }
  89 +
  90 + /**
  91 + * 发送成功,当短信模板关闭时
  92 + */
  93 + @Test
  94 + public void testSendSingleSms_successWhenSmsTemplateDisable() {
  95 + // 准备参数
  96 + String mobile = randomString();
  97 + Long userId = randomLongId();
  98 + Integer userType = randomEle(UserTypeEnum.values()).getValue();
  99 + String templateCode = randomString();
  100 + Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
  101 + .put("op", "login").build();
  102 + // mock SmsTemplateService 的方法
  103 + SysSmsTemplateDO template = randomPojo(SysSmsTemplateDO.class, o -> {
  104 + o.setStatus(CommonStatusEnum.DISABLE.getStatus());
  105 + o.setContent("验证码为{code}, 操作为{op}");
  106 + o.setParams(Lists.newArrayList("code", "op"));
  107 + });
  108 + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
  109 + String content = randomString();
  110 + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
  111 + .thenReturn(content);
  112 + // mock SmsLogService 的方法
  113 + Long smsLogId = randomLongId();
  114 + when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.FALSE), eq(template),
  115 + eq(content), eq(templateParams))).thenReturn(smsLogId);
  116 +
  117 + // 调用
  118 + Long resultSmsLogId = smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams);
  119 + // 断言
  120 + assertEquals(smsLogId, resultSmsLogId);
  121 + // 断言调用
  122 + verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(),
  123 + anyLong(), any(), anyList());
  124 + }
  125 +
  126 + @Test
  127 + public void testCheckSmsTemplateValid_notExists() {
  128 + // 准备参数
  129 + String templateCode = randomString();
  130 + // mock 方法
  131 +
  132 + // 调用,并断言异常
  133 + assertServiceException(() -> smsService.checkSmsTemplateValid(templateCode),
  134 + SMS_TEMPLATE_NOT_EXISTS);
  135 + }
  136 +
  137 + @Test
  138 + public void testBuildTemplateParams_paramMiss() {
  139 + // 准备参数
  140 + SysSmsTemplateDO template = randomPojo(SysSmsTemplateDO.class,
  141 + o -> o.setParams(Lists.newArrayList("code")));
  142 + Map<String, Object> templateParams = new HashMap<>();
  143 + // mock 方法
  144 +
  145 + // 调用,并断言异常
  146 + assertServiceException(() -> smsService.buildTemplateParams(template, templateParams),
  147 + SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, "code");
  148 + }
  149 +
  150 + @Test
  151 + public void testCheckMobile_notExists() {
  152 + // 准备参数
  153 + // mock 方法
  154 +
  155 + // 调用,并断言异常
  156 + assertServiceException(() -> smsService.checkMobile(null),
  157 + SMS_SEND_MOBILE_NOT_EXISTS);
  158 + }
  159 +
  160 + @Test
  161 + @SuppressWarnings("unchecked")
  162 + public void testDoSendSms() {
  163 + // 准备参数
  164 + SysSmsSendMessage message = randomPojo(SysSmsSendMessage.class);
  165 + // mock SmsClientFactory 的方法
  166 + SmsClient smsClient = spy(SmsClient.class);
  167 + when(smsClientFactory.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient);
  168 + // mock SmsClient 的方法
  169 + SmsCommonResult<SmsSendRespDTO> sendResult = randomPojo(SmsCommonResult.class, SmsSendRespDTO.class);
  170 + when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getApiTemplateId()),
  171 + eq(message.getTemplateParams()))).thenReturn(sendResult);
  172 +
  173 + // 调用
  174 + smsService.doSendSms(message);
  175 + // 断言
  176 + verify(smsLogService, times(1)).updateSmsSendResult(eq(message.getLogId()),
  177 + eq(sendResult.getCode()), eq(sendResult.getMsg()), eq(sendResult.getApiCode()),
  178 + eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getData().getSerialNo()));
  179 + }
  180 +
  181 + @Test
  182 + public void testReceiveSmsStatus() throws Throwable {
  183 + // 准备参数
  184 + String channelCode = randomString();
  185 + String text = randomString();
  186 + // mock SmsClientFactory 的方法
  187 + SmsClient smsClient = spy(SmsClient.class);
  188 + when(smsClientFactory.getSmsClient(eq(channelCode))).thenReturn(smsClient);
  189 + // mock SmsClient 的方法
  190 + List<SmsReceiveRespDTO> receiveResults = randomPojoList(SmsReceiveRespDTO.class);
  191 +
  192 + // 调用
  193 + smsService.receiveSmsStatus(channelCode, text);
  194 + // 断言
  195 + receiveResults.forEach(result -> {
  196 + smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()),
  197 + eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode()));
  198 + });
  199 + }
  200 +
  201 +}
src/test/java/cn/iocoder/dashboard/modules/system/service/sms/SysSmsTemplateServiceTest.java 0 → 100644
  1 +package cn.iocoder.dashboard.modules.system.service.sms;
  2 +
  3 +import cn.iocoder.dashboard.BaseDbUnitTest;
  4 +import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
  6 +import cn.iocoder.dashboard.common.pojo.PageResult;
  7 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
  8 +import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
  9 +import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
  10 +import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
  11 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateCreateReqVO;
  12 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateExportReqVO;
  13 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplatePageReqVO;
  14 +import cn.iocoder.dashboard.modules.system.controller.sms.vo.template.SysSmsTemplateUpdateReqVO;
  15 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsChannelDO;
  16 +import cn.iocoder.dashboard.modules.system.dal.dataobject.sms.SysSmsTemplateDO;
  17 +import cn.iocoder.dashboard.modules.system.dal.mysql.sms.SysSmsTemplateMapper;
  18 +import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsTemplateTypeEnum;
  19 +import cn.iocoder.dashboard.modules.system.mq.producer.sms.SysSmsProducer;
  20 +import cn.iocoder.dashboard.modules.system.service.sms.impl.SysSmsTemplateServiceImpl;
  21 +import cn.iocoder.dashboard.util.collection.ArrayUtils;
  22 +import cn.iocoder.dashboard.util.object.ObjectUtils;
  23 +import com.google.common.collect.Lists;
  24 +import org.junit.jupiter.api.Test;
  25 +import org.springframework.boot.test.mock.mockito.MockBean;
  26 +import org.springframework.context.annotation.Import;
  27 +
  28 +import javax.annotation.Resource;
  29 +import java.util.Date;
  30 +import java.util.List;
  31 +import java.util.Map;
  32 +import java.util.function.Consumer;
  33 +
  34 +import static cn.hutool.core.bean.BeanUtil.getFieldValue;
  35 +import static cn.hutool.core.util.RandomUtil.randomEle;
  36 +import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
  37 +import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
  38 +import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
  39 +import static cn.iocoder.dashboard.util.RandomUtils.*;
  40 +import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
  41 +import static cn.iocoder.dashboard.util.object.ObjectUtils.max;
  42 +import static org.junit.jupiter.api.Assertions.*;
  43 +import static org.mockito.ArgumentMatchers.eq;
  44 +import static org.mockito.Mockito.*;
  45 +
  46 +/**
  47 +* {@link SysSmsTemplateServiceImpl} 的单元测试类
  48 +*
  49 +* @author 芋道源码
  50 +*/
  51 +@Import(SysSmsTemplateServiceImpl.class)
  52 +public class SysSmsTemplateServiceTest extends BaseDbUnitTest {
  53 +
  54 + @Resource
  55 + private SysSmsTemplateServiceImpl smsTemplateService;
  56 +
  57 + @Resource
  58 + private SysSmsTemplateMapper smsTemplateMapper;
  59 +
  60 + @MockBean
  61 + private SysSmsChannelService smsChannelService;
  62 + @MockBean
  63 + private SmsClientFactory smsClientFactory;
  64 + @MockBean
  65 + private SmsClient smsClient;
  66 + @MockBean
  67 + private SysSmsProducer smsProducer;
  68 +
  69 + @Test
  70 + @SuppressWarnings("unchecked")
  71 + void testInitLocalCache() {
  72 + // mock 数据
  73 + SysSmsTemplateDO smsTemplate01 = randomSmsTemplateDO();
  74 + smsTemplateMapper.insert(smsTemplate01);
  75 + SysSmsTemplateDO smsTemplate02 = randomSmsTemplateDO();
  76 + smsTemplateMapper.insert(smsTemplate02);
  77 +
  78 + // 调用
  79 + smsTemplateService.initLocalCache();
  80 + // 断言 deptCache 缓存
  81 + Map<String, SysSmsTemplateDO> smsTemplateCache = (Map<String, SysSmsTemplateDO>) getFieldValue(smsTemplateService, "smsTemplateCache");
  82 + assertEquals(2, smsTemplateCache.size());
  83 + assertPojoEquals(smsTemplate01, smsTemplateCache.get(smsTemplate01.getCode()));
  84 + assertPojoEquals(smsTemplate02, smsTemplateCache.get(smsTemplate02.getCode()));
  85 + // 断言 maxUpdateTime 缓存
  86 + Date maxUpdateTime = (Date) getFieldValue(smsTemplateService, "maxUpdateTime");
  87 + assertEquals(max(smsTemplate01.getUpdateTime(), smsTemplate02.getUpdateTime()), maxUpdateTime);
  88 + }
  89 +
  90 + @Test
  91 + public void testParseTemplateContentParams() {
  92 + // 准备参数
  93 + String content = "正在进行登录操作{operation},您的验证码是{code}";
  94 + // mock 方法
  95 +
  96 + // 调用
  97 + List<String> params = smsTemplateService.parseTemplateContentParams(content);
  98 + // 断言
  99 + assertEquals(Lists.newArrayList("operation", "code"), params);
  100 + }
  101 +
  102 + @Test
  103 + @SuppressWarnings("unchecked")
  104 + public void testCreateSmsTemplate_success() {
  105 + // 准备参数
  106 + SysSmsTemplateCreateReqVO reqVO = randomPojo(SysSmsTemplateCreateReqVO.class, o -> {
  107 + o.setContent("正在进行登录操作{operation},您的验证码是{code}");
  108 + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
  109 + o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围
  110 + });
  111 + // mock Channel 的方法
  112 + SysSmsChannelDO channelDO = randomPojo(SysSmsChannelDO.class, o -> {
  113 + o.setId(reqVO.getChannelId());
  114 + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态
  115 + });
  116 + when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO);
  117 + // mock 获得 API 短信模板成功
  118 + when(smsClientFactory.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient);
  119 + when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn(randomPojo(SmsCommonResult.class, SmsTemplateRespDTO.class,
  120 + o -> o.setCode(GlobalErrorCodeConstants.SUCCESS.getCode())));
  121 +
  122 + // 调用
  123 + Long smsTemplateId = smsTemplateService.createSmsTemplate(reqVO);
  124 + // 断言
  125 + assertNotNull(smsTemplateId);
  126 + // 校验记录的属性是否正确
  127 + SysSmsTemplateDO smsTemplate = smsTemplateMapper.selectById(smsTemplateId);
  128 + assertPojoEquals(reqVO, smsTemplate);
  129 + assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams());
  130 + assertEquals(channelDO.getCode(), smsTemplate.getChannelCode());
  131 + // 校验调用
  132 + verify(smsProducer, times(1)).sendSmsTemplateRefreshMessage();
  133 + }
  134 +
  135 + @Test
  136 + @SuppressWarnings("unchecked")
  137 + public void testUpdateSmsTemplate_success() {
  138 + // mock 数据
  139 + SysSmsTemplateDO dbSmsTemplate = randomSmsTemplateDO();
  140 + smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据
  141 + // 准备参数
  142 + SysSmsTemplateUpdateReqVO reqVO = randomPojo(SysSmsTemplateUpdateReqVO.class, o -> {
  143 + o.setId(dbSmsTemplate.getId()); // 设置更新的 ID
  144 + o.setContent("正在进行登录操作{operation},您的验证码是{code}");
  145 + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
  146 + o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围
  147 + });
  148 + // mock 方法
  149 + SysSmsChannelDO channelDO = randomPojo(SysSmsChannelDO.class, o -> {
  150 + o.setId(reqVO.getChannelId());
  151 + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态
  152 + });
  153 + when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO);
  154 + // mock 获得 API 短信模板成功
  155 + when(smsClientFactory.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient);
  156 + when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn(randomPojo(SmsCommonResult.class, SmsTemplateRespDTO.class,
  157 + o -> o.setCode(GlobalErrorCodeConstants.SUCCESS.getCode())));
  158 +
  159 + // 调用
  160 + smsTemplateService.updateSmsTemplate(reqVO);
  161 + // 校验是否更新正确
  162 + SysSmsTemplateDO smsTemplate = smsTemplateMapper.selectById(reqVO.getId()); // 获取最新的
  163 + assertPojoEquals(reqVO, smsTemplate);
  164 + assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams());
  165 + assertEquals(channelDO.getCode(), smsTemplate.getChannelCode());
  166 + // 校验调用
  167 + verify(smsProducer, times(1)).sendSmsTemplateRefreshMessage();
  168 + }
  169 +
  170 + @Test
  171 + public void testUpdateSmsTemplate_notExists() {
  172 + // 准备参数
  173 + SysSmsTemplateUpdateReqVO reqVO = randomPojo(SysSmsTemplateUpdateReqVO.class);
  174 +
  175 + // 调用, 并断言异常
  176 + assertServiceException(() -> smsTemplateService.updateSmsTemplate(reqVO), SMS_TEMPLATE_NOT_EXISTS);
  177 + }
  178 +
  179 + @Test
  180 + public void testDeleteSmsTemplate_success() {
  181 + // mock 数据
  182 + SysSmsTemplateDO dbSmsTemplate = randomSmsTemplateDO();
  183 + smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据
  184 + // 准备参数
  185 + Long id = dbSmsTemplate.getId();
  186 +
  187 + // 调用
  188 + smsTemplateService.deleteSmsTemplate(id);
  189 + // 校验数据不存在了
  190 + assertNull(smsTemplateMapper.selectById(id));
  191 + // 校验调用
  192 + verify(smsProducer, times(1)).sendSmsTemplateRefreshMessage();
  193 + }
  194 +
  195 + @Test
  196 + public void testDeleteSmsTemplate_notExists() {
  197 + // 准备参数
  198 + Long id = randomLongId();
  199 +
  200 + // 调用, 并断言异常
  201 + assertServiceException(() -> smsTemplateService.deleteSmsTemplate(id), SMS_TEMPLATE_NOT_EXISTS);
  202 + }
  203 +
  204 + @Test
  205 + public void testGetSmsTemplatePage() {
  206 + // mock 数据
  207 + SysSmsTemplateDO dbSmsTemplate = randomPojo(SysSmsTemplateDO.class, o -> { // 等会查询到
  208 + o.setType(SysSmsTemplateTypeEnum.PROMOTION.getType());
  209 + o.setStatus(CommonStatusEnum.ENABLE.getStatus());
  210 + o.setCode("yudaoyuanma");
  211 + o.setContent("芋道源码");
  212 + o.setApiTemplateId("yunai");
  213 + o.setChannelId(1L);
  214 + o.setCreateTime(buildTime(2021, 11, 11));
  215 + });
  216 + smsTemplateMapper.insert(dbSmsTemplate);
  217 + // 测试 type 不匹配
  218 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setType(SysSmsTemplateTypeEnum.VERIFICATION_CODE.getType())));
  219 + // 测试 status 不匹配
  220 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
  221 + // 测试 code 不匹配
  222 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setCode("yuanma")));
  223 + // 测试 content 不匹配
  224 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setContent("源码")));
  225 + // 测试 apiTemplateId 不匹配
  226 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setApiTemplateId("nai")));
  227 + // 测试 channelId 不匹配
  228 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setChannelId(2L)));
  229 + // 测试 createTime 不匹配
  230 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12))));
  231 + // 准备参数
  232 + SysSmsTemplatePageReqVO reqVO = new SysSmsTemplatePageReqVO();
  233 + reqVO.setType(SysSmsTemplateTypeEnum.PROMOTION.getType());
  234 + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
  235 + reqVO.setCode("yudao");
  236 + reqVO.setContent("芋道");
  237 + reqVO.setApiTemplateId("yu");
  238 + reqVO.setChannelId(1L);
  239 + reqVO.setBeginCreateTime(buildTime(2021, 11, 1));
  240 + reqVO.setEndCreateTime(buildTime(2021, 12, 1));
  241 +
  242 + // 调用
  243 + PageResult<SysSmsTemplateDO> pageResult = smsTemplateService.getSmsTemplatePage(reqVO);
  244 + // 断言
  245 + assertEquals(1, pageResult.getTotal());
  246 + assertEquals(1, pageResult.getList().size());
  247 + assertPojoEquals(dbSmsTemplate, pageResult.getList().get(0));
  248 + }
  249 +
  250 + @Test
  251 + public void testGetSmsTemplateList() {
  252 + // mock 数据
  253 + SysSmsTemplateDO dbSmsTemplate = randomPojo(SysSmsTemplateDO.class, o -> { // 等会查询到
  254 + o.setType(SysSmsTemplateTypeEnum.PROMOTION.getType());
  255 + o.setStatus(CommonStatusEnum.ENABLE.getStatus());
  256 + o.setCode("yudaoyuanma");
  257 + o.setContent("芋道源码");
  258 + o.setApiTemplateId("yunai");
  259 + o.setChannelId(1L);
  260 + o.setCreateTime(buildTime(2021, 11, 11));
  261 + });
  262 + smsTemplateMapper.insert(dbSmsTemplate);
  263 + // 测试 type 不匹配
  264 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setType(SysSmsTemplateTypeEnum.VERIFICATION_CODE.getType())));
  265 + // 测试 status 不匹配
  266 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
  267 + // 测试 code 不匹配
  268 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setCode("yuanma")));
  269 + // 测试 content 不匹配
  270 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setContent("源码")));
  271 + // 测试 apiTemplateId 不匹配
  272 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setApiTemplateId("nai")));
  273 + // 测试 channelId 不匹配
  274 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setChannelId(2L)));
  275 + // 测试 createTime 不匹配
  276 + smsTemplateMapper.insert(ObjectUtils.clone(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12))));
  277 + // 准备参数
  278 + SysSmsTemplateExportReqVO reqVO = new SysSmsTemplateExportReqVO();
  279 + reqVO.setType(SysSmsTemplateTypeEnum.PROMOTION.getType());
  280 + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
  281 + reqVO.setCode("yudao");
  282 + reqVO.setContent("芋道");
  283 + reqVO.setApiTemplateId("yu");
  284 + reqVO.setChannelId(1L);
  285 + reqVO.setBeginCreateTime(buildTime(2021, 11, 1));
  286 + reqVO.setEndCreateTime(buildTime(2021, 12, 1));
  287 +
  288 + // 调用
  289 + List<SysSmsTemplateDO> list = smsTemplateService.getSmsTemplateList(reqVO);
  290 + // 断言
  291 + assertEquals(1, list.size());
  292 + assertPojoEquals(dbSmsTemplate, list.get(0));
  293 + }
  294 +
  295 + @Test
  296 + public void testCheckSmsChannel_success() {
  297 + // 准备参数
  298 + Long channelId = randomLongId();
  299 + // mock 方法
  300 + SysSmsChannelDO channelDO = randomPojo(SysSmsChannelDO.class, o -> {
  301 + o.setId(channelId);
  302 + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态
  303 + });
  304 + when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO);
  305 +
  306 + // 调用
  307 + SysSmsChannelDO returnChannelDO = smsTemplateService.checkSmsChannel(channelId);
  308 + // 断言
  309 + assertPojoEquals(returnChannelDO, channelDO);
  310 + }
  311 +
  312 + @Test
  313 + public void testCheckSmsChannel_notExists() {
  314 + // 准备参数
  315 + Long channelId = randomLongId();
  316 +
  317 + // 调用,校验异常
  318 + assertServiceException(() -> smsTemplateService.checkSmsChannel(channelId),
  319 + SMS_CHANNEL_NOT_EXISTS);
  320 + }
  321 +
  322 + @Test
  323 + public void testCheckSmsChannel_disable() {
  324 + // 准备参数
  325 + Long channelId = randomLongId();
  326 + // mock 方法
  327 + SysSmsChannelDO channelDO = randomPojo(SysSmsChannelDO.class, o -> {
  328 + o.setId(channelId);
  329 + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); // 保证 status 禁用,触发失败
  330 + });
  331 + when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO);
  332 +
  333 + // 调用,校验异常
  334 + assertServiceException(() -> smsTemplateService.checkSmsChannel(channelId),
  335 + SMS_CHANNEL_DISABLE);
  336 + }
  337 +
  338 + @Test
  339 + public void testCheckDictDataValueUnique_success() {
  340 + // 调用,成功
  341 + smsTemplateService.checkSmsTemplateCodeDuplicate(randomLongId(), randomString());
  342 + }
  343 +
  344 + @Test
  345 + public void testCheckSmsTemplateCodeDuplicate_valueDuplicateForCreate() {
  346 + // 准备参数
  347 + String code = randomString();
  348 + // mock 数据
  349 + smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code)));
  350 +
  351 + // 调用,校验异常
  352 + assertServiceException(() -> smsTemplateService.checkSmsTemplateCodeDuplicate(null, code),
  353 + SMS_TEMPLATE_CODE_DUPLICATE, code);
  354 + }
  355 +
  356 + @Test
  357 + public void testCheckDictDataValueUnique_valueDuplicateForUpdate() {
  358 + // 准备参数
  359 + Long id = randomLongId();
  360 + String code = randomString();
  361 + // mock 数据
  362 + smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code)));
  363 +
  364 + // 调用,校验异常
  365 + assertServiceException(() -> smsTemplateService.checkSmsTemplateCodeDuplicate(id, code),
  366 + SMS_TEMPLATE_CODE_DUPLICATE, code);
  367 + }
  368 +
  369 + // ========== 随机对象 ==========
  370 +
  371 + @SafeVarargs
  372 + private static SysSmsTemplateDO randomSmsTemplateDO(Consumer<SysSmsTemplateDO>... consumers) {
  373 + Consumer<SysSmsTemplateDO> consumer = (o) -> {
  374 + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
  375 + o.setType(randomEle(SysSmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围
  376 + };
  377 + return randomPojo(SysSmsTemplateDO.class, ArrayUtils.append(consumer, consumers));
  378 + }
  379 +
  380 +}
src/test/java/cn/iocoder/dashboard/util/AopTargetUtils.java 0 → 100644
  1 +package cn.iocoder.dashboard.util;
  2 +
  3 +import cn.hutool.core.bean.BeanUtil;
  4 +import org.springframework.aop.framework.AdvisedSupport;
  5 +import org.springframework.aop.framework.AopProxy;
  6 +import org.springframework.aop.support.AopUtils;
  7 +
  8 +/**
  9 + * Spring AOP 工具类
  10 + *
  11 + * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现
  12 + */
  13 +public class AopTargetUtils {
  14 +
  15 + /**
  16 + * 获取代理的目标对象
  17 + *
  18 + * @param proxy 代理对象
  19 + * @return 目标对象
  20 + */
  21 + public static Object getTarget(Object proxy) throws Exception {
  22 + // 不是代理对象
  23 + if (!AopUtils.isAopProxy(proxy)) {
  24 + return proxy;
  25 + }
  26 + // Jdk 代理
  27 + if (AopUtils.isJdkDynamicProxy(proxy)) {
  28 + return getJdkDynamicProxyTargetObject(proxy);
  29 + }
  30 + // Cglib 代理
  31 + return getCglibProxyTargetObject(proxy);
  32 + }
  33 +
  34 + private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
  35 + Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0");
  36 + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised");
  37 + return advisedSupport.getTargetSource().getTarget();
  38 + }
  39 +
  40 + private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
  41 + AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h");
  42 + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised");
  43 + return advisedSupport.getTargetSource().getTarget();
  44 + }
  45 +
  46 +}
src/test/java/cn/iocoder/dashboard/util/AssertUtils.java
@@ -10,6 +10,7 @@ import org.junit.jupiter.api.function.Executable; @@ -10,6 +10,7 @@ import org.junit.jupiter.api.function.Executable;
10 10
11 import java.lang.reflect.Field; 11 import java.lang.reflect.Field;
12 import java.util.Arrays; 12 import java.util.Arrays;
  13 +import java.util.Objects;
13 14
14 import static org.junit.jupiter.api.Assertions.assertThrows; 15 import static org.junit.jupiter.api.Assertions.assertThrows;
15 16
@@ -51,6 +52,33 @@ public class AssertUtils { @@ -51,6 +52,33 @@ public class AssertUtils {
51 } 52 }
52 53
53 /** 54 /**
  55 + * 比对两个对象的属性是否一致
  56 + *
  57 + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略
  58 + *
  59 + * @param expected 期望对象
  60 + * @param actual 实际对象
  61 + * @param ignoreFields 忽略的属性数组
  62 + * @return 是否一致
  63 + */
  64 + public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) {
  65 + Field[] expectedFields = ReflectUtil.getFields(expected.getClass());
  66 + return Arrays.stream(expectedFields).allMatch(expectedField -> {
  67 + // 如果是忽略的属性,则不进行比对
  68 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) {
  69 + return true;
  70 + }
  71 + // 忽略不存在的属性
  72 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName());
  73 + if (actualField == null) {
  74 + return true;
  75 + }
  76 + return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField),
  77 + ReflectUtil.getFieldValue(actual, actualField));
  78 + });
  79 + }
  80 +
  81 + /**
54 * 执行方法,校验抛出的 Service 是否符合条件 82 * 执行方法,校验抛出的 Service 是否符合条件
55 * 83 *
56 * @param executable 业务异常 84 * @param executable 业务异常
@@ -62,7 +90,7 @@ public class AssertUtils { @@ -62,7 +90,7 @@ public class AssertUtils {
62 ServiceException serviceException = assertThrows(ServiceException.class, executable); 90 ServiceException serviceException = assertThrows(ServiceException.class, executable);
63 // 校验错误码 91 // 校验错误码
64 Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); 92 Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配");
65 - String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMessage(), messageParams); 93 + String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams);
66 Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配"); 94 Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配");
67 } 95 }
68 96
src/test/java/cn/iocoder/dashboard/util/RandomUtils.java
@@ -7,9 +7,8 @@ import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO; @@ -7,9 +7,8 @@ import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
7 import uk.co.jemos.podam.api.PodamFactory; 7 import uk.co.jemos.podam.api.PodamFactory;
8 import uk.co.jemos.podam.api.PodamFactoryImpl; 8 import uk.co.jemos.podam.api.PodamFactoryImpl;
9 9
10 -import java.util.Arrays;  
11 -import java.util.Date;  
12 -import java.util.Set; 10 +import java.lang.reflect.Type;
  11 +import java.util.*;
13 import java.util.function.Consumer; 12 import java.util.function.Consumer;
14 import java.util.stream.Collectors; 13 import java.util.stream.Collectors;
15 import java.util.stream.Stream; 14 import java.util.stream.Stream;
@@ -87,4 +86,21 @@ public class RandomUtils { @@ -87,4 +86,21 @@ public class RandomUtils {
87 return pojo; 86 return pojo;
88 } 87 }
89 88
  89 + @SafeVarargs
  90 + public static <T> T randomPojo(Class<T> clazz, Type type, Consumer<T>... consumers) {
  91 + T pojo = PODAM_FACTORY.manufacturePojo(clazz, type);
  92 + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理
  93 + if (ArrayUtil.isNotEmpty(consumers)) {
  94 + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo));
  95 + }
  96 + return pojo;
  97 + }
  98 +
  99 + @SafeVarargs
  100 + public static <T> List<T> randomPojoList(Class<T> clazz, Consumer<T>... consumers) {
  101 + int size = RandomUtil.randomInt(0, RANDOM_COLLECTION_LENGTH);
  102 + return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers))
  103 + .collect(Collectors.toList());
  104 + }
  105 +
90 } 106 }
src/test/resources/sql/clean.sql
@@ -19,3 +19,6 @@ DELETE FROM &quot;sys_post&quot;; @@ -19,3 +19,6 @@ DELETE FROM &quot;sys_post&quot;;
19 DELETE FROM "sys_login_log"; 19 DELETE FROM "sys_login_log";
20 DELETE FROM "sys_operate_log"; 20 DELETE FROM "sys_operate_log";
21 DELETE FROM "sys_user"; 21 DELETE FROM "sys_user";
  22 +DELETE FROM "sys_sms_channel";
  23 +DELETE FROM "sys_sms_template";
  24 +DELETE FROM "sys_sms_log";
src/test/resources/sql/create_tables.sql
@@ -47,8 +47,7 @@ CREATE TABLE IF NOT EXISTS &quot;inf_job&quot; ( @@ -47,8 +47,7 @@ CREATE TABLE IF NOT EXISTS &quot;inf_job&quot; (
47 PRIMARY KEY ("id") 47 PRIMARY KEY ("id")
48 ) COMMENT='定时任务表'; 48 ) COMMENT='定时任务表';
49 49
50 -DROP TABLE IF EXISTS "inf_job_log";  
51 -CREATE TABLE "inf_job_log" ( 50 +CREATE TABLE IF NOT EXISTS "inf_job_log" (
52 "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '日志编号', 51 "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '日志编号',
53 "job_id" bigint(20) NOT NULL COMMENT '任务编号', 52 "job_id" bigint(20) NOT NULL COMMENT '任务编号',
54 "handler_name" varchar(64) NOT NULL COMMENT '处理器的名字', 53 "handler_name" varchar(64) NOT NULL COMMENT '处理器的名字',
@@ -192,8 +191,7 @@ CREATE TABLE IF NOT EXISTS `sys_user_session` ( @@ -192,8 +191,7 @@ CREATE TABLE IF NOT EXISTS `sys_user_session` (
192 PRIMARY KEY (`id`) 191 PRIMARY KEY (`id`)
193 ) COMMENT '用户在线 Session'; 192 ) COMMENT '用户在线 Session';
194 193
195 -CREATE TABLE IF NOT EXISTS "sys_post"  
196 -( 194 +CREATE TABLE IF NOT EXISTS "sys_post" (
197 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, 195 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
198 "code" varchar(64) NOT NULL, 196 "code" varchar(64) NOT NULL,
199 "name" varchar(50) NOT NULL, 197 "name" varchar(50) NOT NULL,
@@ -208,7 +206,6 @@ CREATE TABLE IF NOT EXISTS &quot;sys_post&quot; @@ -208,7 +206,6 @@ CREATE TABLE IF NOT EXISTS &quot;sys_post&quot;
208 PRIMARY KEY ("id") 206 PRIMARY KEY ("id")
209 ) COMMENT '岗位信息表'; 207 ) COMMENT '岗位信息表';
210 208
211 -  
212 CREATE TABLE IF NOT EXISTS "sys_notice" ( 209 CREATE TABLE IF NOT EXISTS "sys_notice" (
213 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, 210 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
214 "title" varchar(50) NOT NULL COMMENT '公告标题', 211 "title" varchar(50) NOT NULL COMMENT '公告标题',
@@ -223,7 +220,6 @@ CREATE TABLE IF NOT EXISTS &quot;sys_notice&quot; ( @@ -223,7 +220,6 @@ CREATE TABLE IF NOT EXISTS &quot;sys_notice&quot; (
223 PRIMARY KEY("id") 220 PRIMARY KEY("id")
224 ) COMMENT '通知公告表'; 221 ) COMMENT '通知公告表';
225 222
226 -  
227 CREATE TABLE IF NOT EXISTS `sys_login_log` ( 223 CREATE TABLE IF NOT EXISTS `sys_login_log` (
228 `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, 224 `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY,
229 `log_type` bigint(4) NOT NULL, 225 `log_type` bigint(4) NOT NULL,
@@ -240,7 +236,6 @@ CREATE TABLE IF NOT EXISTS `sys_login_log` ( @@ -240,7 +236,6 @@ CREATE TABLE IF NOT EXISTS `sys_login_log` (
240 PRIMARY KEY (`id`) 236 PRIMARY KEY (`id`)
241 ) COMMENT ='系统访问记录'; 237 ) COMMENT ='系统访问记录';
242 238
243 -  
244 CREATE TABLE IF NOT EXISTS `sys_operate_log` ( 239 CREATE TABLE IF NOT EXISTS `sys_operate_log` (
245 `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, 240 `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY,
246 `trace_id` varchar(64) NOT NULL DEFAULT '', 241 `trace_id` varchar(64) NOT NULL DEFAULT '',
@@ -346,3 +341,73 @@ CREATE TABLE IF NOT EXISTS &quot;inf_api_error_log&quot; ( @@ -346,3 +341,73 @@ CREATE TABLE IF NOT EXISTS &quot;inf_api_error_log&quot; (
346 "deleted" bit not null default false, 341 "deleted" bit not null default false,
347 primary key ("id") 342 primary key ("id")
348 ) COMMENT '系统异常日志'; 343 ) COMMENT '系统异常日志';
  344 +
  345 +CREATE TABLE IF NOT EXISTS "sys_sms_channel" (
  346 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  347 + "signature" varchar(10) NOT NULL,
  348 + "code" varchar(63) NOT NULL,
  349 + "status" tinyint NOT NULL,
  350 + "remark" varchar(255) DEFAULT NULL,
  351 + "api_key" varchar(63) NOT NULL,
  352 + "api_secret" varchar(63) DEFAULT NULL,
  353 + "callback_url" varchar(255) DEFAULT NULL,
  354 + "creator" varchar(64) DEFAULT '',
  355 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  356 + "updater" varchar(64) DEFAULT '',
  357 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  358 + "deleted" bit NOT NULL DEFAULT FALSE,
  359 + PRIMARY KEY ("id")
  360 +) COMMENT '短信渠道';
  361 +
  362 +CREATE TABLE "sys_sms_template" (
  363 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  364 + "type" tinyint NOT NULL,
  365 + "status" tinyint NOT NULL,
  366 + "code" varchar(63) NOT NULL,
  367 + "name" varchar(63) NOT NULL,
  368 + "content" varchar(255) NOT NULL,
  369 + "params" varchar(255) NOT NULL,
  370 + "remark" varchar(255) DEFAULT NULL,
  371 + "api_template_id" varchar(63) NOT NULL,
  372 + "channel_id" bigint NOT NULL,
  373 + "channel_code" varchar(63) NOT NULL,
  374 + "creator" varchar(64) DEFAULT '',
  375 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  376 + "updater" varchar(64) DEFAULT '',
  377 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  378 + "deleted" bit NOT NULL DEFAULT FALSE,
  379 + PRIMARY KEY ("id")
  380 +) COMMENT '短信模板';
  381 +
  382 +CREATE TABLE "sys_sms_log" (
  383 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  384 + "channel_id" bigint NOT NULL,
  385 + "channel_code" varchar(63) NOT NULL,
  386 + "template_id" bigint NOT NULL,
  387 + "template_code" varchar(63) NOT NULL,
  388 + "template_type" tinyint NOT NULL,
  389 + "template_content" varchar(255) NOT NULL,
  390 + "template_params" varchar(255) NOT NULL,
  391 + "api_template_id" varchar(63) NOT NULL,
  392 + "mobile" varchar(11) NOT NULL,
  393 + "user_id" bigint DEFAULT '0',
  394 + "user_type" tinyint DEFAULT '0',
  395 + "send_status" tinyint NOT NULL DEFAULT '0',
  396 + "send_time" timestamp DEFAULT NULL,
  397 + "send_code" int DEFAULT NULL,
  398 + "send_msg" varchar(255) DEFAULT NULL,
  399 + "api_send_code" varchar(63) DEFAULT NULL,
  400 + "api_send_msg" varchar(255) DEFAULT NULL,
  401 + "api_request_id" varchar(255) DEFAULT NULL,
  402 + "api_serial_no" varchar(255) DEFAULT NULL,
  403 + "receive_status" tinyint NOT NULL DEFAULT '0',
  404 + "receive_time" timestamp DEFAULT NULL,
  405 + "api_receive_code" varchar(63) DEFAULT NULL,
  406 + "api_receive_msg" varchar(255) DEFAULT NULL,
  407 + "creator" varchar(64) DEFAULT '',
  408 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  409 + "updater" varchar(64) DEFAULT '',
  410 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  411 + "deleted" bit NOT NULL DEFAULT FALSE,
  412 + PRIMARY KEY ("id")
  413 +) COMMENT '短信日志';