UserInfoRepository.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2024 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace app\common\repositories\user;
  12. use app\common\dao\user\UserInfoDao;
  13. use app\common\dao\user\UserSignDao;
  14. use app\common\repositories\BaseRepository;
  15. use app\common\repositories\system\config\ConfigValueRepository;
  16. use FormBuilder\Factory\Elm;
  17. use Swoole\Database\MysqliException;
  18. use think\exception\ValidateException;
  19. use think\facade\Db;
  20. use think\facade\Route;
  21. /**
  22. * @mixin UserInfoDao
  23. */
  24. class UserInfoRepository extends BaseRepository
  25. {
  26. protected $type = [
  27. 'input' => ['label' => '文本', 'type' => 'varchar(255)', 'value' => '', 'default' => ''],
  28. 'int' => ['label' => '数字', 'type' => 'int(11)', 'value' => 0, 'default' => 0],
  29. 'phone' => ['label' => '手机号', 'type' => 'varchar(11)', 'value' => '', 'default' => ''],
  30. 'date' => ['label' => '时间', 'type' => 'date', 'value' => null,'default' => 'NULL'],
  31. 'radio' => ['label' => '单选', 'type' => 'tinyint(1)', 'value' => 0, 'default' => 0],
  32. 'address' => ['label' => '地址', 'type' => 'varchar(255)', 'value' => '', 'default' => ''],
  33. 'id_card' => ['label' => '身份证', 'type' => 'varchar(255)', 'value' => '', 'default' => ''],
  34. 'email' => ['label' => '邮箱', 'type' => 'varchar(255)', 'value' => '', 'default' => ''],
  35. ];
  36. const FIXED_FIELD = ['uid'];
  37. /**
  38. * @var UserInfoDao
  39. */
  40. protected $dao;
  41. /**
  42. * UserSignRepository constructor.
  43. * @param UserInfoDao $dao
  44. */
  45. public function __construct(UserInfoDao $dao)
  46. {
  47. $this->dao = $dao;
  48. }
  49. // 验证是否是有效的列名
  50. /**
  51. * 检查列名是否有效。
  52. *
  53. * 该方法用于验证传入的列名是否符合规定的格式。列名必须以字母开头,后续可以是字母、数字或下划线的组合。
  54. * 如果列名不符合这个规则,将抛出一个ValidateException异常,指出列名无效。
  55. *
  56. * @param string $columnName 待验证的列名。
  57. * @throws ValidateException 如果列名无效,则抛出此异常。
  58. */
  59. protected function isValidColumnName($columnName)
  60. {
  61. // 使用正则表达式检查列名是否以字母开头,且仅包含字母、数字和下划线
  62. // 检查是否以字母开头,只包含字母、数字和下划线
  63. if (!preg_match('/^[a-z][a-z0-9_]*$/', $columnName)) {
  64. throw new ValidateException("无效的列名: {$columnName}");
  65. }
  66. }
  67. /**
  68. * 执行SQL语句。
  69. *
  70. * 本函数用于执行给定的SQL语句,通过传递数据库连接和SQL语句参数来完成。
  71. * 如果在执行SQL语句时遇到MysqliException异常,它将被捕获并重新抛出为ValidateException异常。
  72. * 这种异常处理机制允许上层调用者更统一地处理异常情况,尤其是数据库操作相关的异常。
  73. *
  74. * @param $db PDO 实例,用于执行SQL语句的数据库连接。
  75. * @param $sql string,待执行的SQL语句。
  76. * @param $params array,默认为空数组,包含SQL语句中需要的参数。
  77. * @throws ValidateException 如果执行SQL语句时发生错误,则抛出此异常。
  78. */
  79. public function executeSql($db, $sql, $params = [])
  80. {
  81. try {
  82. $db->execute($sql, $params);
  83. } catch (MysqliException $exception) {
  84. throw new ValidateException($exception->getMessage());
  85. }
  86. }
  87. /**
  88. * 创建字段
  89. * @param array $data
  90. * @param $type
  91. * @return bool
  92. *
  93. * @date 2023/09/25
  94. * @author yyw
  95. */
  96. protected function changeField(array $data, $type = true)
  97. {
  98. // 获取数据库连接实例
  99. $db = Db::connect();
  100. //操作的数据表
  101. $tableName = env('database.prefix', 'eb_') . 'user_fields';
  102. // 操作的字段名称
  103. $fieldName = $data['field'];
  104. // 验证字段端名是否合法
  105. $this->isValidColumnName($fieldName);
  106. //
  107. if ($type) {
  108. // 字段类型,可以根据需要进行修改
  109. $fieldType = $this->type[$data['type']]['type'] ?? 'varchar(255)';
  110. $comment = $data['title'] ?: $data['msg'];
  111. $default = $this->type[$data['type']]['default'];
  112. $sql = "ALTER TABLE `$tableName` ADD COLUMN `$fieldName` $fieldType NULL DEFAULT '$default' COMMENT '$comment'";
  113. } else {
  114. $sql = "ALTER TABLE `$tableName` DROP COLUMN `$fieldName`";
  115. }
  116. $this->executeSql($db, $sql);
  117. return true;
  118. }
  119. /**
  120. * 删除字段
  121. * @param string $fieldName
  122. * @return bool
  123. *
  124. * @date 2023/09/25
  125. * @author yyw
  126. */
  127. protected function deleteField(string $fieldName)
  128. {
  129. // 验证字段名称是否有效
  130. $this->isValidColumnName($fieldName);
  131. // 获取数据库连接实例
  132. $db = Db::connect();
  133. // 操作的数据表
  134. $tableName = env('database.prefix', 'eb_') . 'user_fields';
  135. // 构建 SQL 删除字段的语句
  136. $sql = "ALTER TABLE `$tableName` DROP COLUMN `$fieldName`";
  137. $this->executeSql($db, $sql);
  138. return true;
  139. }
  140. /**
  141. * 获取列表数据
  142. * 根据给定的条件和分页参数,从数据库中获取列表数据,并进行相关处理。
  143. *
  144. * @param array $where 查询条件,用于筛选数据。
  145. * @param int $page 当前页码,用于分页查询。
  146. * @param int $limit 每页数据条数,用于分页查询。
  147. * @return array 返回包含数据总数、数据列表和默认头像信息的数组。
  148. */
  149. public function getList(array $where = [], int $page = 1, int $limit = 10)
  150. {
  151. // 根据条件获取查询对象
  152. $query = $this->dao->getSearch($where);
  153. // 计算满足条件的数据总数
  154. $count = $query->count();
  155. // 获取排序后的数据列表
  156. $list = $query->order(['sort', 'create_time' => 'ASC'])->select()->each(function ($item) {
  157. // 给数据项的type_name赋值,来源于type数组中对应type的label值
  158. return $item->type_name = $this->type[$item->type]['label'];
  159. });
  160. // 获取系统配置的默认用户头像地址
  161. $avatar = systemConfig('user_default_avatar');
  162. // 返回包含数据总数、处理后的数据列表和默认头像信息的数组
  163. return compact('count', 'list', 'avatar');
  164. }
  165. /**
  166. * 创建表单用于添加字段信息
  167. *
  168. * 该方法通过Element UI的表单构建器创建一个表单,用于添加系统用户信息字段。
  169. * 表单包括字段类型的选择、字段的名称、键值和提示信息的输入。
  170. * 其中字段类型为单选框,选项通过getType方法获取,当选择字段类型为'radio'时,需要额外输入配置内容。
  171. *
  172. * @return \Encore\Admin\Widgets\Form|\FormBuilder\Form
  173. */
  174. public function createFrom()
  175. {
  176. // 构建表单提交的URL
  177. $action = Route::buildUrl('systemUserInfoCreate')->build();
  178. // 使用Elm表单构建器创建表单,并设置表单字段
  179. $form = Elm::createForm($action, [
  180. // 字段类型选择器,包括获取字段类型的选项和针对'radio'类型字段的额外配置输入
  181. Elm::select('type', '字段类型:')->options($this->getType())->control([
  182. [
  183. 'value' => 'radio',
  184. 'rule' => [
  185. Elm::textarea('content', '配置内容')->placeholder('请输入配置内容')->required()
  186. ]
  187. ]
  188. ])->required(),
  189. // 字段名称输入框
  190. Elm::input('title', '字段名称:')->placeholder('请输入字段名称')->required(),
  191. // 字段键值输入框
  192. Elm::input('field', '字段key:')->placeholder('请输入字段key')->required(),
  193. // 提示信息输入框
  194. Elm::input('msg', '提示信息:')->placeholder('请输入提示信息')->required()
  195. ]);
  196. // 设置表单标题
  197. return $form->setTitle('添加字段');
  198. }
  199. /**
  200. * 创建字段
  201. *
  202. * 本函数用于根据传入的数据创建新的字段。它首先验证字段类型是否有效,然后确保对于单选字段类型(radio),
  203. * 至少提供了两个选项。如果验证失败,将抛出一个验证异常。然后,它将内容数据编码为JSON格式,并为字段设置默认的排序值。
  204. * 最后,它在数据库事务中执行字段的创建操作。
  205. *
  206. * @param array $data 包含字段相关信息的数组,包括字段类型、内容等。
  207. * @return bool|void 返回数据库事务执行结果,如果执行成功,则返回true,否则不返回任何值。
  208. * @throws ValidateException 如果字段类型无效或单选字段选项不足两个,将抛出此异常。
  209. */
  210. public function create(array $data)
  211. {
  212. // 验证字段类型是否在定义的类型数组中
  213. // 验证类型
  214. if (!isset($this->type[$data['type']])) {
  215. throw new ValidateException('字段类型异常');
  216. }
  217. // 对于单选字段类型(radio),确保至少有两个选项
  218. if ($data['type'] == 'radio' && (!is_array($data['content']) || count($data['content']) < 2)) {
  219. throw new ValidateException('请至少创建两个选项');
  220. }
  221. // 将内容数据编码为JSON格式
  222. $data['content'] = json_encode($data['content']);
  223. // 设置默认的排序值
  224. $data['sort'] = 999;
  225. // 使用数据库事务执行字段的创建操作,包括更改字段信息和实际创建字段的操作
  226. return Db::transaction(function () use ($data) {
  227. $this->changeField($data);
  228. $this->dao->create($data);
  229. });
  230. }
  231. /**
  232. * 保存用户信息及扩展字段配置
  233. *
  234. * 本函数主要用于处理用户信息的保存操作,其中包括用户头像信息的更新,
  235. * 以及用户扩展信息字段的配置更新。通过对传入数据的处理,确保了用户
  236. * 信息的完整性和扩展字段的正确配置。
  237. *
  238. * @param array $data 用户信息及扩展字段数据数组
  239. * 包含 'avatar' 用户头像信息
  240. * 包含 'user_extend_info' 用户扩展信息字段数组
  241. * @return bool 返回保存操作的结果,true表示保存成功
  242. */
  243. public function saveAll(array $data = [])
  244. {
  245. // 设置用户默认头像信息
  246. app()->make(ConfigValueRepository::class)->setFormData(['user_default_avatar' => $data['avatar']], 0);
  247. // 检查并更新用户扩展信息字段的配置
  248. // 保存用户表单
  249. if (!empty($data['user_extend_info'])) {
  250. foreach ($data['user_extend_info'] as $sort => $item) {
  251. // 确保必要参数存在,避免空值导致的错误
  252. if (!isset($item['field']) || !isset($item['is_used']) || !isset($item['is_require']) || !isset($item['is_show'])) {
  253. throw new ValidateException('参数不能为空');
  254. }
  255. // 确保参数值的合法性,只允许特定的取值范围
  256. if (!in_array($item['is_used'], [0, 1]) || !in_array($item['is_require'], [0, 1]) || !in_array($item['is_show'], [0, 1])) {
  257. throw new ValidateException('参数类性错误');
  258. }
  259. // 更新扩展字段的配置信息
  260. $this->dao->query([])->where('field', $item['field'])->update(['is_used' => $item['is_used'], 'is_require' => $item['is_require'], 'is_show' => $item['is_show'], 'sort' => $sort]);
  261. }
  262. }
  263. return true;
  264. }
  265. /**
  266. * 删除表单数据。
  267. *
  268. * 本函数用于根据给定的ID删除表单数据。在删除前,它会进行一系列的验证以确保数据的安全性和完整性。
  269. * 首先,它会检查所要删除的数据是否存在以及'field'字段是否为空,如果存在异常情况,则会抛出一个验证异常。
  270. * 其次,它会检查待删除的数据是否为默认数据,如果是默认数据,则同样会抛出一个验证异常,以防止删除重要的默认设置。
  271. * 最后,如果所有验证通过,函数将使用事务来确保删除操作的原子性,即先删除字段数据,再删除实际的表单数据。
  272. *
  273. * @param int $id 表单数据的唯一标识ID。
  274. * @return bool 如果删除成功,返回true。
  275. * @throws ValidateException 如果数据不存在、字段为空或尝试删除默认表单数据,则抛出此异常。
  276. */
  277. public function delete(int $id)
  278. {
  279. // 通过ID获取表单信息
  280. $info = $this->dao->get($id);
  281. // 检查获取的表单信息是否为空,或者字段是否为空,如果是,则抛出异常
  282. if (empty($info) || empty($info['field'])) {
  283. throw new ValidateException('表单数据异常');
  284. }
  285. // 检查是否为默认表单,如果是,则抛出异常,防止删除默认表单
  286. if ($info['is_default']) {
  287. throw new ValidateException('默认表单不能删除');
  288. }
  289. // 使用事务来确保删除操作的原子性
  290. return Db::transaction(function () use ($info, $id) {
  291. // 删除字段数据
  292. $this->deleteField($info['field']);
  293. // 删除实际的表单数据
  294. $this->dao->delete($id);
  295. });
  296. }
  297. /**
  298. * 获取类型信息
  299. *
  300. * 本方法旨在通过转换内部类型数组,返回一个格式化后的类型信息数组。每个类型信息包括
  301. * 值和描述,其中值对应于类型标识,描述则来自于类型数组中的相应元素。
  302. *
  303. * @return array 返回一个格式化后的类型信息数组,每个元素包含 'value' 和 'desc' 两个键,
  304. * 'value' 表示类型的标识,'desc' 是对应的类型描述。
  305. */
  306. public function getType()
  307. {
  308. // 初始化一个空数组,用于存放转换后的类型信息
  309. $res = [];
  310. // 遍历内部类型数组,对每个类型进行处理
  311. foreach ($this->type as $k => $v) {
  312. // 将类型标识作为值赋给当前类型信息
  313. $v['value'] = $k;
  314. // 移除原始类型数组中的'type'键,避免冗余信息
  315. unset($v['type']);
  316. // 将处理后的类型信息添加到结果数组中
  317. $res[] = $v;
  318. }
  319. // 返回转换后的类型信息数组
  320. return $res;
  321. }
  322. /**
  323. * 获取选择列表
  324. *
  325. * 本函数用于检索数据库中特定条件下的数据,这些数据主要用于生成前端的下拉选择列表。
  326. * 具体来说,它查询了被标记为已使用且类型不为'radio'或'date'的条目,并按排序和创建时间升序返回它们的标题和字段值。
  327. *
  328. * @return array 返回一个数组,其中每个元素包含标签(label)和值(value),用于生成选择列表。
  329. */
  330. public function getSelectList()
  331. {
  332. // 使用DAO对象进行数据库查询,设置条件以获取已使用且类型不是'radio'或'date'的记录
  333. // 排序方式为按排序字段和创建时间升序
  334. // 最后,只返回标题作为标签和字段作为值的列
  335. return $this->dao->getSearch(['is_used' => 1])
  336. ->whereNotIn('type', ['radio', 'date'])
  337. ->order(['sort', 'create_time' => 'ASC'])
  338. ->column('title as label,field as value');
  339. }
  340. /**
  341. * 验证字段是否为默认字段
  342. * @param string $fields
  343. * @return bool
  344. */
  345. public function getFieldsIsItDefault(string $field)
  346. {
  347. return (bool)$this->getSearch(['field' => $field])->value('is_default', 0);
  348. }
  349. }