From d1bf6f0cc1a962dbb52350c3806b9ac537ba3441 Mon Sep 17 00:00:00 2001
From: panliang <1163816051@qq.com>
Date: Wed, 31 Aug 2022 13:17:53 +0800
Subject: [PATCH] init
---
.gitignore | 7 +
README.md | 60 +++++
composer.json | 33 +++
.../2022_08_11_110611_create_users_table.php | 70 ++++++
lang/en/user-balance.php | 3 +
lang/en/user.php | 5 +
lang/zh_CN/user-balance.php | 15 ++
lang/zh_CN/user.php | 27 +++
routes/admin.php | 14 ++
routes/api.php | 28 +++
src/Enums/SocialiteType.php | 48 ++++
src/Events/UserRegister.php | 20 ++
src/Filters/UserFilter.php | 15 ++
src/Http/Admin/UserController.php | 184 +++++++++++++++
src/Http/Api/AuthController.php | 221 ++++++++++++++++++
src/Http/Api/UserController.php | 40 ++++
src/Http/Resources/UserResource.php | 32 +++
src/Http/Resources/UserTinyResource.php | 30 +++
src/Models/User.php | 122 ++++++++++
src/Models/UserSocialite.php | 21 ++
src/UserServiceProvider.php | 27 +++
21 files changed, 1022 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 composer.json
create mode 100644 database/2022_08_11_110611_create_users_table.php
create mode 100644 lang/en/user-balance.php
create mode 100644 lang/en/user.php
create mode 100644 lang/zh_CN/user-balance.php
create mode 100644 lang/zh_CN/user.php
create mode 100644 routes/admin.php
create mode 100644 routes/api.php
create mode 100644 src/Enums/SocialiteType.php
create mode 100644 src/Events/UserRegister.php
create mode 100644 src/Filters/UserFilter.php
create mode 100644 src/Http/Admin/UserController.php
create mode 100644 src/Http/Api/AuthController.php
create mode 100644 src/Http/Api/UserController.php
create mode 100644 src/Http/Resources/UserResource.php
create mode 100644 src/Http/Resources/UserTinyResource.php
create mode 100644 src/Models/User.php
create mode 100644 src/Models/UserSocialite.php
create mode 100644 src/UserServiceProvider.php
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9d4b362
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+phpunit.phar
+/vendor
+composer.phar
+composer.lock
+*.project
+.idea/
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a3ee2cf
--- /dev/null
+++ b/README.md
@@ -0,0 +1,60 @@
+# Dcat Admin Extension
+
+用户管理
+
+## 安装
+
+- `composer config repositories.peidikeji/dcat-admin-user git git@gitee.com:paddy_technology/dcat-admin-user.git`
+- `composer require peidikeji/dcat-admin-user:dev-develop`
+- `php artisan vendor:publish --tag=dcat-admin-user-migrations`
+
+## 事件
+
+- 用户注册成功: `Peidikeji\User\Events\UserRegister`
+
+## 数据表
+
+### 用户表: users
+
+| column | type | nullable | default | comment |
+| - | - | - | - | - |
+| id | bigint | not null | - | 主键 |
+| username | varchar(191) | null | - | 用户名 |
+| phone | varchar(191) | null | - | 手机号 |
+| name | varchar(191) | null | - | 昵称 |
+| avatar | varchar(191) | null | - | 头像 |
+| balance | decimal(12, 2) | not null | 0 | 余额 |
+| invite_code | varchar(191) | not null | - | 邀请码 |
+| inviter_id | bigint | null | - | 邀请人 |
+| inviter_path | varchart(191) | null | - | 所有的上级邀请人(-1-2-3-) |
+| created_at | timestamp | null | - | 创建时间 |
+| updated_at | timestamp | null | - | 更新时间 |
+
+### 第三方登录信息: user_socialites
+
+| column | type | nullable | default | comment |
+| - | - | - | - | - |
+| id | bigint | not null | - | 主键 |
+| user_id | bigint | not null | - | 外键关联 users.id |
+| type | varchar(191) | not null | - | 类型(SocialiteType) |
+| openid | varchar(191) | not null | - | 第三方唯一凭证 |
+| unionid | varchar(191) | null | - | 第三方唯一凭证 |
+| data | json | null | - | 第三方数据 |
+| created_at | timestamp | null | - | 创建时间 |
+| updated_at | timestamp | null | - | 更新时间 |
+
+### 用户余额变动记录: user_balance_logs
+
+| column | type | nullable | default | comment |
+| - | - | - | - | - |
+| id | bigint | not null | - | 主键 |
+| user_id | bigint | not null | - | 外键关联 users.id |
+| cate | varchar(191) | not null | - | 类别 |
+| description | varchar(191) | not null | - | 描述 |
+| amount | decimal(12, 2) | not null | - | 变动数量(正数为增加, 负数为减少) |
+| balance | decimal(12, 2) | not null | - | 变动后的余额 |
+| remarks | varchart(191) | null | - | 备注 |
+| source_type | varchart(191) | null | - | 来源(多态关联) |
+| source_id | bigint | null | - | 来源(多态关联) |
+| created_at | timestamp | null | - | 创建时间 |
+| updated_at | timestamp | null | - | 更新时间 |
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..8120d5c
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,33 @@
+{
+ "name": "peidikeji/dcat-admin-user",
+ "alias": "用户管理",
+ "description": "用户管理",
+ "type": "library",
+ "keywords": ["dcat-admin", "extension", "user"],
+ "homepage": "https://gitee.com/paddy_technology/dcat-admin-user",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "panliang",
+ "email": "1163816051@qq.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.0",
+ "peidikeji/dcat-admin": "*",
+ "tucker-eric/eloquentfilter": "^3.1",
+ "laravel/framework": "^9.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Peidikeji\\User\\": "src/"
+ }
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Peidikeji\\User\\UserServiceProvider"
+ ]
+ }
+ }
+}
diff --git a/database/2022_08_11_110611_create_users_table.php b/database/2022_08_11_110611_create_users_table.php
new file mode 100644
index 0000000..7066be5
--- /dev/null
+++ b/database/2022_08_11_110611_create_users_table.php
@@ -0,0 +1,70 @@
+id();
+ $table->string('username');
+ $table->string('password')->nullable();
+ $table->string('phone')->nullable();
+ $table->string('name')->nullable();
+ $table->string('avatar')->nullable();
+ $table->decimal('balance', 12, 2)->default(0)->comment('余额');
+ $table->string('invite_code')->comment('邀请码');
+ $table->unsignedBigInteger('inviter_id')->nullable()->comment('邀请人');
+ $table->string('inviter_path')->nullable()->comment('所有上级邀请人');
+ $table->timestamps();
+
+ $table->comment('用户表');
+ });
+
+ Schema::create('user_socialites', function (Blueprint $table) {
+ $table->id();
+ $table->unsignedBigInteger('user_id');
+ $table->string('type');
+ $table->string('openid');
+ $table->string('unionid')->nullable();
+ $table->json('data')->nullable();
+ $table->timestamps();
+
+ $table->comment('用户-第三方登录');
+ });
+
+ Schema::create('user_balance_logs', function (Blueprint $table) {
+ $table->id();
+ $table->unsignedBigInteger('user_id');
+ $table->string('cate')->comment('类别');
+ $table->string('description')->comment('描述');
+ $table->decimal('amount', 12, 2)->comment('变动数量, 正数为增加, 负数为减少');
+ $table->decimal('balance', 12, 2)->comment('变更后的余额');
+ $table->string('remarks')->nullable()->comment('备注');
+ $table->nullableMorphs('source');
+ $table->timestamps();
+
+ $table->comment('用户-余额流水记录');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('user_balance_logs');
+ Schema::dropIfExists('user_socialites');
+ Schema::dropIfExists('users');
+ }
+};
diff --git a/lang/en/user-balance.php b/lang/en/user-balance.php
new file mode 100644
index 0000000..0b67a5f
--- /dev/null
+++ b/lang/en/user-balance.php
@@ -0,0 +1,3 @@
+ [
+ 'UserBalance' => '余额流水',
+ 'user-balance' => '余额流水',
+ ],
+ 'fields' => [
+ 'user_id' => '用户',
+ 'cate' => '类别',
+ 'amount' => '金额',
+ 'description' => '描述',
+ 'created_at' => '时间',
+ ]
+];
diff --git a/lang/zh_CN/user.php b/lang/zh_CN/user.php
new file mode 100644
index 0000000..0a79a2e
--- /dev/null
+++ b/lang/zh_CN/user.php
@@ -0,0 +1,27 @@
+ [
+ 'User' => '用户管理',
+ 'user' => '用户',
+ 'users' => '用户',
+ ],
+ 'fields' => [
+ 'id' => 'ID',
+ 'name' => '姓名',
+ 'gender' => '性别',
+ 'phone' => '手机号',
+ 'avatar' => '头像',
+ 'balance' => '余额',
+ 'profit' => 'e品额',
+ 'inviter_id' => '邀请人',
+ 'inviter' => [
+ 'name' => '邀请人',
+ 'phone' => '邀请人',
+ ],
+ 'invite_code' => '邀请码',
+ 'username' => '用户名',
+ 'password' => '密码',
+ 'created_at' => '注册时间',
+ ]
+];
diff --git a/routes/admin.php b/routes/admin.php
new file mode 100644
index 0000000..44fa555
--- /dev/null
+++ b/routes/admin.php
@@ -0,0 +1,14 @@
+ config('admin.route.prefix'),
+ 'middleware' => config('admin.route.middleware'),
+], function () {
+ Route::get('api/user', [UserController::class, 'list'])->name('dcat.admin.api.users');
+
+ Route::resource('users', UserController::class)->names('dcat.admin.users');
+});
diff --git a/routes/api.php b/routes/api.php
new file mode 100644
index 0000000..fd82249
--- /dev/null
+++ b/routes/api.php
@@ -0,0 +1,28 @@
+ ['api'],
+ 'prefix' => 'api',
+], function () {
+ Route::group(['prefix' => 'auth'], function () {
+ Route::post('login', [AuthController::class, 'login']);
+ Route::post('register', [AuthController::class, 'register']);
+
+ Route::post('login-by-sms', [AuthController::class, 'loginBySms']);
+
+ Route::post('login-by-wxmini', [AuthController::class, 'loginByWxMini']);
+ Route::post('wx-bind-phone', [AuthController::class, 'wxbindPhone']);
+
+ Route::post('reset', [AuthController::class, 'reset']);
+ Route::post('reset-pwd', [AuthController::class, 'resetPwd']);
+ });
+
+ Route::group(['prefix' => 'user', 'middleware' => ['auth:api']], function () {
+ Route::get('profile', [UserController::class, 'profile']);
+ Route::put('profile', [UserController::class, 'update']);
+ });
+});
diff --git a/src/Enums/SocialiteType.php b/src/Enums/SocialiteType.php
new file mode 100644
index 0000000..e2e5772
--- /dev/null
+++ b/src/Enums/SocialiteType.php
@@ -0,0 +1,48 @@
+value => '微信小程序',
+ ];
+ }
+
+ public function text()
+ {
+ return data_get(self::options(), $this->value);
+ }
+
+ public function label()
+ {
+ $color = match ($this) {
+ static::WxMini => 'success',
+ };
+
+ $background = Admin::color()->get($color, $color);
+
+ $name = static::options()[$this->value] ?? '其它';
+
+ return "{$name}";
+ }
+
+ public function dot()
+ {
+ $color = match ($this) {
+ static::WxMini => 'success',
+ };
+
+ $background = Admin::color()->get($color, $color);
+
+ $name = static::options()[$this->value] ?? '其它';
+
+ return " {$name}";
+ }
+}
diff --git a/src/Events/UserRegister.php b/src/Events/UserRegister.php
new file mode 100644
index 0000000..a4f1cdf
--- /dev/null
+++ b/src/Events/UserRegister.php
@@ -0,0 +1,20 @@
+user = $user;
+ }
+}
diff --git a/src/Filters/UserFilter.php b/src/Filters/UserFilter.php
new file mode 100644
index 0000000..6194426
--- /dev/null
+++ b/src/Filters/UserFilter.php
@@ -0,0 +1,15 @@
+where(function ($q) use ($key) {
+ $q->orWhere('phone', 'like', "%$key%")->orWhere('username', 'like', "%$key%");
+ });
+ }
+}
diff --git a/src/Http/Admin/UserController.php b/src/Http/Admin/UserController.php
new file mode 100644
index 0000000..3c5e10e
--- /dev/null
+++ b/src/Http/Admin/UserController.php
@@ -0,0 +1,184 @@
+filled('q')) {
+ $search = '%' . $request->input('q'). '%';
+ $query->where(fn($q) => $q->where('phone', 'like', $search)->orWhere('name', 'like', $search));
+ }
+
+ if ($request->filled('none_merchant') || $request->filled('amp;none_merchant')) {
+ $query->whereDoesntHave('merchants');
+ }
+
+ $query->select(['id', 'phone as text']);
+
+ if ($request->filled('_paginate')) {
+ $list = $query->paginate();
+ } else {
+ $list = $query->get();
+ }
+ return $list;
+ }
+
+ protected function grid()
+ {
+ return Grid::make(User::with(['inviter']), function (Grid $grid) {
+ $grid->model()->sort();
+
+ $grid->disableRowSelector();
+
+ $grid->column('id');
+ $grid->column('name')->display(function () {
+ return ($this->avatar ? '
' : '') . $this->name;
+ });
+ $grid->column('phone');
+ $grid->column('balance')->modal(__('dcat-admin-user::user.fields.balance') . '变更记录', fn() => BalanceLogTable::make(['type' => BalanceLog::TYPE_BALANCE, 'subject_id' => $this->id, 'subject_type' => 'user']));
+ $grid->column('profit')->modal(__('dcat-admin-user::user.fields.profit') . '变更记录', fn() => BalanceLogTable::make(['type' => BalanceLog::TYPE_PROFIT, 'subject_id' => $this->id, 'subject_type' => 'user']));
+ $grid->column('inviter.phone');
+ $grid->column('created_at');
+
+ $user = Admin::user();
+ $grid->showCreateButton($user->can('dcat.admin.users.create'));
+ $grid->showViewButton($user->can('dcat.admin.users.show'));
+ $grid->showEditButton($user->can('dcat.admin.users.edit'));
+ $grid->showDeleteButton($user->can('dcat.admin.users.destroy'));
+ $grid->actions(function (Grid\Displayers\Actions $actions) use ($user) {
+ //余额变动
+ if($user->can('dcat.admin.users.change_balance')){
+ $actions->append(new UserBalanceChange());
+ }
+ });
+
+ $grid->filter(function (Filter $filter) {
+ $filter->panel();
+ $filter->where('name', function ($q) {
+ $search = '%'.$this->input.'%';
+ $q->where(fn($q) => $q->where('username', 'like', $search)->orWhere('name', 'like', $search)->orWhere('phone', 'like', $search));
+ }, '搜索')->placeholder('姓名/手机号')->width(3);
+ });
+ });
+ }
+
+ protected function form()
+ {
+ return Form::make(new User(), function (Form $form) {
+ $uniquePhone = Rule::unique('users', 'phone');
+ if ($form->isEditing()) {
+ $uniquePhone->ignore($form->model()->id);
+ }
+ $form->text('phone')->rules([$uniquePhone])->required();
+ $form->text('name')->required();
+ $form->image('avatar')->autoUpload()->saveFullUrl()->move('user/avatar');
+ $form->select('inviter_id')->model(User::class, 'id', 'name')->ajax('api/user?_paginate=1');
+ $form->hidden('inviter_path');
+ $form->hidden('invite_code');
+
+ $form->saving(function (Form $form) {
+ $form->inviter_path = $form->inviter_id ? User::where('id', $form->inviter_id)->value('inviter_path') . $form->inviter_id.'-' : null;
+ if (!$form->invite_code) {
+ do {
+ $invite_code = strtoupper(Str::random(6));
+ } while(User::where('invite_code', $invite_code)->exists());
+ $form->invite_code = $invite_code;
+ }
+ });
+
+ $form->deleting(function (Form $form) {
+ $data = $form->model()->toArray();
+ $ids = array_column($data, 'id');
+ if (Merchant::whereIn('user_id', $ids)->exists()) {
+ return $form->response()->error('已经关联店铺, 无法删除');
+ }
+ if (Order::whereIn('user_id', $ids)->exists()) {
+ return $form->response()->error('已经关联订单, 请先删除订单');
+ }
+ // 删除用户的登录信息
+ UserSocialite::whereIn('user_id', $ids)->delete();
+ foreach($ids as $id) {
+ $current = User::findOrFail($id);
+ // 删除用户的邀请信息
+ $users = User::where('inviter_path', 'like', '%-'.$id.'-%')->get();
+ foreach($users as $user) {
+ $user->inviter_path = str_replace('-'.$id.'-', '-', $user->inviter_path);
+ $ids = explode('-', $user->inviter_path);
+ $user->inviter_id = count($ids) > 2 ? $ids[count($ids) - 2] : null;
+ $user->save();
+ }
+ }
+ });
+ });
+ }
+
+ protected function detail($id)
+ {
+ $info = User::with(['inviter', 'inviteUsers', 'socialites'])->findOrFail($id);
+ $show = Show::make($info);
+ $show->field('name');
+ $show->field('gender');
+ $show->field('phone');
+ $show->field('avatar')->image('', 64);
+ $show->field('inviter.phone');
+ $show->field('invite_code');
+ $show->field('balance');
+ $show->field('profit');
+ $show->field('created_at');
+ $show->tools(function (Tools $tools) {
+ $tools->disableBack();
+ $tools->disableList(false);
+ });
+
+ $tab = new Tab();
+
+ $keys = ['id', 'username', 'name', 'phone', 'created_at'];
+ $inviteUsers = $info->inviteUsers->map(function ($item) {
+ return [$item->id, $item->username, $item->name, $item->phone, $item->created_at->format('Y-m-d H:i:s')];
+ });
+ $headers = array_map(fn($v) => __($this->translation . '.fields.' . $v), $keys);
+ $table = new Table($headers, $inviteUsers, ['table-hover']);
+ $tab->add('邀请用户', $table);
+
+ $keys = ['type', 'openid', 'created_at'];
+ $socialites = $info->socialites->map(function ($item) {
+ return [$item->type->text(), $item->openid, $item->created_at->format('Y-m-d H:i:s')];
+ });
+ $table = new Table($keys, $socialites, ['table-hover']);
+ $tab->add('绑定账户', $table);
+
+ $row = new Row();
+ $row->column(6, $show);
+ $row->column(6, (new Card('', $tab)));
+ return $row;
+ }
+}
diff --git a/src/Http/Api/AuthController.php b/src/Http/Api/AuthController.php
new file mode 100644
index 0000000..c1d07d0
--- /dev/null
+++ b/src/Http/Api/AuthController.php
@@ -0,0 +1,221 @@
+validate([
+ 'username' => 'required',
+ 'password' => 'required',
+ ]);
+
+ $user = User::filter(['key' => $request->input('username')])->first();
+ if (!$user) {
+ return $this->error('用户名或密码错误');
+ }
+ if (!Hash::check($request->input('password'), $user->password)) {
+ return $this->error('用户名或密码错误1');
+ }
+
+ return $this->attemptUser($user);
+ }
+
+ public function loginBySms(Request $request)
+ {
+ $request->validate([
+ 'phone' => 'required',
+ 'code' => 'required',
+ ]);
+ $phone = $request->input('phone');
+
+ $result = Sms::checkCode('login', $phone, $request->input('code'));
+ if (!$result) {
+ return $this->error('验证码不正确或已过期');
+ }
+
+ $user = User::where('phone', $phone)->first();
+ if (!$user) {
+ $user = $this->createUser(['phone' => $phone], $request->input('invite_code'));
+ }
+
+ return $this->attemptUser($user);
+ }
+
+ /**
+ * 微信小程序登录
+ * https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
+ */
+ public function loginByWxMini(Request $request)
+ {
+ $request->validate([
+ 'code' => 'required',
+ ]);
+ $app = EasyWeChat::miniApp();
+ $response = $app->getClient()->get('/sns/jscode2session', [
+ 'appid' => $app->getAccount()->getAppId(),
+ 'secret' => $app->getAccount()->getSecret(),
+ 'js_code' => $request->input('code'),
+ 'grant_type' => 'authorization_code',
+ ]);
+ $result = $response->toArray();
+ $openid = data_get($result, 'openid');
+ if (!$openid) {
+ return $this->error('未获取到 openid');
+ }
+ $unionid = data_get($result, 'unionid');
+ $socialite = UserSocialite::query()->updateOrCreate([
+ 'openid' => $openid,
+ 'type' => SocialiteType::WxMini,
+ ], [
+ 'data' => $result,
+ 'unionid' => $unionid,
+ ]);
+ if ($socialite->user) {
+ return $this->attemptUser($socialite->user);
+ }
+
+ return $this->json(['openid' => $openid]);
+ }
+
+ /**
+ * 微信小程序获取手机号
+ * https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html
+ */
+ public function wxbindPhone(Request $request)
+ {
+ $request->validate([
+ 'openid' => 'required',
+ 'code' => 'required',
+ ]);
+ $response = EasyWeChat::miniApp()->getClient()->postJson('/wxa/business/getuserphonenumber', ['code' => $request->input('code')]);
+ $result = $response->toArray();
+ if (data_get($result, 'errcode') !== 0) {
+ return $this->error(data_get($result, 'errmsg'));
+ }
+ $phone = data_get($result, 'phone_info.purePhoneNumber');
+ if (!$phone) {
+ return $this->error('未获取到手机号');
+ }
+
+ $user = User::where('phone', $phone)->first();
+ if (!$user) {
+ $user = $this->createUser(['phone' => $phone], $request->input('invite_code'));
+ } elseif ($user->socialites()->where('type', SocialiteType::WxMini)->exists()) {
+ return $this->error('手机号已经绑定过了');
+ }
+
+ $openid = $request->input('openid');
+ UserSocialite::updateOrCreate([
+ 'openid' => $openid,
+ 'type' => SocialiteType::WxMini,
+ ], [
+ 'user_id' => $user->id,
+ ]);
+
+ return $this->attemptUser($user);
+ }
+
+ public function register(Request $request)
+ {
+ $request->validate([
+ 'phone' => 'required',
+ 'code' => 'required',
+ ]);
+ $phone = $request->input('phone');
+
+ if (User::where('phone', $phone)->exists()) {
+ return $this->error('用户已经注册');
+ }
+
+ $result = Sms::checkCode('register', $phone, $request->input('code'));
+ if (!$result) {
+ return $this->error('验证码不正确或已过期');
+ }
+
+ $user = $this->createUser(['phone' => $phone], $request->input('invite_code'));
+
+ return $this->attemptUser($user);
+ }
+
+ public function reset(Request $request)
+ {
+ $request->validate([
+ 'phone' => 'required',
+ 'code' => 'required',
+ 'password' => ['required', Password::min(6)],
+ ]);
+ $phone = $request->input('phone');
+
+ $result = Sms::checkCode('reset', $phone, $request->input('code'));
+ if (!$result) {
+ return $this->error('验证码不正确或已过期');
+ }
+
+ $user = User::where('phone', $phone)->first();
+ if (!$user) {
+ return $this->error('用户不存在');
+ }
+
+ $user->update([
+ 'password' => Hash::make($request->input('password')),
+ ]);
+
+ return $this->success('密码重置成功');
+ }
+
+ protected function attemptUser(User $user, $name = 'api')
+ {
+ $token = $user->createToken($name)->plainTextToken;
+ return $this->json(['token' => $token, 'id' => $user->id]);
+ }
+
+ protected function createUser($attributes = [], $invite_code = null)
+ {
+ if ($invite_code) {
+ $inviterId = User::where('invite_code', $invite_code)->value('id');
+ if (!$inviterId) {
+ throw new BizException('邀请码错误');
+ }
+ $attributes['inviter_id'] = $inviterId;
+ }
+ $user = User::create($attributes);
+
+ event(new UserRegister($user));
+
+ return $user;
+ }
+
+ public function resetPwd(Request $request)
+ {
+ $input = $request->validate([
+ 'password' => 'required|current_password:api',
+ 'new_password' => 'required',
+ ], [
+ 'password.current_password' => '密码错误',
+ ]);
+ $user = auth('api')->user();
+ if (!$user || !Hash::check($input['password'], $user->password)) {
+ throw new BizException('密码错误');
+ }
+
+ $user->password = bcrypt($input['new_password']);
+ $user->save();
+
+ $user->tokens()->delete();
+ return $this->success('修改成功');
+ }
+}
diff --git a/src/Http/Api/UserController.php b/src/Http/Api/UserController.php
new file mode 100644
index 0000000..8627834
--- /dev/null
+++ b/src/Http/Api/UserController.php
@@ -0,0 +1,40 @@
+user();
+
+ return UserResource::make($user);
+ }
+
+ public function update(Request $request)
+ {
+ $update = $request->validate([
+ 'name' => 'string',
+ 'avatar' => 'string',
+ 'gender' => 'string',
+ 'password' => 'confirmed',
+ ]);
+ $user = auth('api')->user();
+
+ if ($request->filled('password')) {
+ $validatePassword = !is_null($user->password);
+ if ($validatePassword) {
+ $request->validate(['password_old' => 'current_password:api'], ['password_old.current_password' => '原密码错误']);
+ }
+ $update['password'] = Hash::make($request->input('password'));
+ }
+
+ $user->update($update);
+ return $this->success('修改成功');
+ }
+}
diff --git a/src/Http/Resources/UserResource.php b/src/Http/Resources/UserResource.php
new file mode 100644
index 0000000..9ac7c8c
--- /dev/null
+++ b/src/Http/Resources/UserResource.php
@@ -0,0 +1,32 @@
+ $this->id,
+ 'name' => $this->name,
+ 'username' => $this->username,
+ 'avatar' => $this->avatar,
+ 'balance' => $this->balance,
+ 'profit' => $this->profit,
+ 'invite_code' => $this->invite_code,
+ 'inviter_id' => $this->inviter_id,
+ 'phone' => $this->phone,
+ 'gender' => $this->gender,
+ 'vip_expired_at' => $this->vip_expired_at?->timestamp,
+ 'created_at' => $this->created_at?->timestamp,
+ ];
+ }
+
+ public function with($request)
+ {
+ return ['code' => Response::HTTP_OK, 'message' => ''];
+ }
+}
diff --git a/src/Http/Resources/UserTinyResource.php b/src/Http/Resources/UserTinyResource.php
new file mode 100644
index 0000000..f9c09ee
--- /dev/null
+++ b/src/Http/Resources/UserTinyResource.php
@@ -0,0 +1,30 @@
+ $this->id,
+ 'name' => $this->name,
+ 'username' => $this->username,
+ 'avatar' => $this->avatar,
+ 'invite_code' => $this->invite_code,
+ 'inviter_id' => $this->inviter_id,
+ 'phone' => $this->phone ? substr_replace($this->phone, '****', 3, 4) : $this->phone,
+ 'created_at' => $this->created_at?->timestamp,
+ 'is_vip' => $this->isVip(),
+ 'vip_expired_at' => $this->vip_expired_at?->timestamp,
+ ];
+ }
+
+ public function with($request)
+ {
+ return ['code' => Response::HTTP_OK, 'message' => ''];
+ }
+}
diff --git a/src/Models/User.php b/src/Models/User.php
new file mode 100644
index 0000000..76ec9d0
--- /dev/null
+++ b/src/Models/User.php
@@ -0,0 +1,122 @@
+invite_code) {
+ do {
+ $invite_code = strtoupper(Str::random(6));
+ } while(User::where('invite_code', $invite_code)->exists());
+ $model->invite_code = $invite_code;
+ }
+ if ($model->inviter_id && !$model->inviter_path) {
+ $inviter_path = User::where('id', $model->inviter_id)->value('inviter_path') ?: '-';
+ $model->inviter_path = $inviter_path . $model->inviter_id . '-';
+ }
+ });
+ }
+
+ public function modelFilter()
+ {
+ return UserFilter::class;
+ }
+
+ public function inviter()
+ {
+ return $this->belongsTo(static::class, 'inviter_id');
+ }
+
+ public function inviteUsers()
+ {
+ return $this->hasMany(static::class, 'inviter_id')->sort();
+ }
+
+ public function socialites()
+ {
+ return $this->hasMany(UserSocialite::class, 'user_id');
+ }
+
+ public function balanceLogs()
+ {
+ return $this->morphMany(BalanceLog::class, 'subject');
+ }
+
+ public function cashOutLogs()
+ {
+ return $this->hasMany(CashOutLog::class, 'user_id');
+ }
+
+ public function merchants()
+ {
+ return $this->hasMany(Merchant::class, 'user_id');
+ }
+
+ public function scopeSort($q)
+ {
+ return $q->latest('id');
+ }
+
+ public function isVip(): bool
+ {
+ return $this->vip_expired_at && $this->vip_expired_at->gt(now()) ? true : false;
+ }
+
+ public function getSubPhone()
+ {
+ // 18223350967 => 0967
+ return $this->phone ? substr($this->phone, 7, 4) : '';
+ }
+
+ public function getHidePhone()
+ {
+ // 18223350967 => 182****0967
+ return $this->phone ? substr_replace($this->phone, '****', 3, 4) : '';
+ }
+
+ public function addresses(){
+ return $this->hasMany(UserAddress::class, 'user_id');
+ }
+
+ // 购物车
+ public function carts()
+ {
+ return $this->hasMany(GoodsCart::class, 'user_id');
+ }
+
+ // 订单
+ public function orders()
+ {
+ return $this->hasMany(Order::class, 'user_id');
+ }
+
+ public function likes()
+ {
+ return $this->hasMany(UserLike::class, 'user_id');
+ }
+}
diff --git a/src/Models/UserSocialite.php b/src/Models/UserSocialite.php
new file mode 100644
index 0000000..2151f89
--- /dev/null
+++ b/src/Models/UserSocialite.php
@@ -0,0 +1,21 @@
+ 'array',
+ 'type' => SocialiteType::class
+ ];
+
+ public function user()
+ {
+ return $this->belongsTo(User::class, 'user_id');
+ }
+}
diff --git a/src/UserServiceProvider.php b/src/UserServiceProvider.php
new file mode 100644
index 0000000..b520209
--- /dev/null
+++ b/src/UserServiceProvider.php
@@ -0,0 +1,27 @@
+loadRoutesFrom(__DIR__.'/../routes/admin.php');
+ $this->loadRoutesFrom(__DIR__.'/../routes/api.php');
+
+ // $this->loadMigrationsFrom(__DIR__.'/../database/');
+
+ $this->publishes([
+ __DIR__.'/../database/' => database_path('migrations')
+ ], 'dcat-admin-user-migrations');
+
+ $this->loadTranslationsFrom(__DIR__.'/../lang', 'dcat-admin-user');
+ }
+}