小程序用户改用门店登录

This commit is contained in:
liu
2026-08-20 13:46:04 +08:00
parent 6d9f02d831
commit c1d490134e
251 changed files with 689 additions and 2727 deletions
@@ -7,7 +7,6 @@ use App\Http\Requests\Customer\CustomerLevelFormRequest;
use App\Models\CustomerLevelModel;
use App\Models\NoticeModel;
use App\Models\StoreModel;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -114,18 +113,13 @@ class CustomerLevelController extends BaseController
}
/**
* 上浮比例变更后,给该等级下正常门店绑定的正常用户生成价格变更通知
* 上浮比例变更后,给该等级下全部正常门店生成价格变更通知
*/
private function notifyPriceChange(CustomerLevelModel $level): void
{
$userIds = UserModel::query()
->where('status', UserModel::STATUS_NORMAL)
->whereIn('store_id', function ($q) use ($level) {
$q->select('id')
->from('store')
->where('status', StoreModel::STATUS_NORMAL)
->where('level_id', $level->id);
})
$storeIds = StoreModel::query()
->where('status', StoreModel::STATUS_NORMAL)
->where('level_id', $level->id)
->pluck('id');
$content = mb_substr(
@@ -134,9 +128,9 @@ class CustomerLevelController extends BaseController
500
);
foreach ($userIds as $userId) {
foreach ($storeIds as $storeId) {
NoticeModel::create([
'user_id' => $userId,
'store_id' => $storeId,
'type' => NoticeModel::TYPE_PRICE,
'title' => '商品价格变更',
'content' => $content,
@@ -1,80 +0,0 @@
<?php
namespace App\Http\Controllers\Customer;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Customer\MiniUserBindRequest;
use App\Models\StoreModel;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 小程序用户管理(用户由小程序登录自动生成,后台只做绑定与状态管理,无增删)
*/
#[RequestAttribute('/customer/miniUser', 'customer.miniUser')]
class MiniUserController extends BaseController
{
protected array $searchField = [
'store_id' => '=',
'status' => '=',
];
protected array $quickSearchField = ['nickname', 'phone'];
/** 小程序用户列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch($params, UserModel::query()->with('store:id,name'))
->orderBy('id', 'desc')
->paginate($pageSize)
->toArray();
return $this->success($data);
}
/**
* 绑定门店(一个门店可绑多个账号,一个账号只绑一个门店)
*/
#[PutRoute(route: '/{id}/bind', authorize: 'bind', where: ['id' => '[0-9]+'])]
public function bind(int $id, MiniUserBindRequest $request): JsonResponse
{
$validated = $request->validated();
$user = UserModel::find($id);
if (empty($user)) {
throw new RepositoryException('用户不存在');
}
$store = StoreModel::find((int) $validated['store_id']);
if (empty($store)) {
throw new RepositoryException('门店不存在');
}
$user->store_id = $store->id;
$user->save();
return $this->success();
}
/** 启用/停用 */
#[PutRoute(route: '/{id}/status', authorize: 'update', where: ['id' => '[0-9]+'])]
public function status(int $id, Request $request): JsonResponse
{
$data = $request->validate([
'status' => 'required|integer|in:0,1',
], [
'status.required' => '状态不能为空',
'status.in' => '状态值不正确',
]);
$user = UserModel::find($id);
if (empty($user)) {
throw new RepositoryException('用户不存在');
}
$user->status = (int) $data['status'];
$user->save();
return $this->success();
}
}
@@ -14,7 +14,7 @@ use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 通知管理(小程序端消息;user_id=0 为全员广播)
* 通知管理(小程序端消息;store_id=0 为全员广播)
*/
#[RequestAttribute('/customer/notice', 'customer.notice')]
class NoticeController extends BaseController
@@ -22,7 +22,7 @@ class NoticeController extends BaseController
protected array $searchField = [
'type' => '=',
'is_read' => '=',
'user_id' => '=',
'store_id' => '=',
];
protected array $quickSearchField = ['title', 'content'];
@@ -40,7 +40,7 @@ class NoticeController extends BaseController
return $this->success($data);
}
/** 发布通知(user_id=0 全员广播) */
/** 发布通知(store_id=0 全员广播) */
#[PostRoute(authorize: 'create')]
public function create(NoticeFormRequest $request): JsonResponse
{
@@ -15,7 +15,7 @@ use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
/**
* 门店管理(小程序下单主体,即客户)
* 门店管理(小程序下单主体,即客户;门店账号即小程序登录账号
*/
#[RequestAttribute('/customer/store', 'customer.store')]
class StoreController extends BaseController
@@ -23,13 +23,14 @@ class StoreController extends BaseController
protected array $searchField = [
'name' => 'like',
'code' => 'like',
'username' => 'like',
'level_id' => '=',
'status' => '=',
];
protected array $quickSearchField = ['name', 'code', 'contact', 'phone'];
protected array $quickSearchField = ['name', 'code', 'username', 'contact', 'phone'];
/** 门店列表(含等级名回显) */
/** 门店列表(含等级名回显;密码永不回显 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
@@ -42,17 +43,18 @@ class StoreController extends BaseController
return $this->success($data);
}
/** 创建门店 */
/** 创建门店(同时设置小程序登录账号与初始密码) */
#[PostRoute(authorize: 'create')]
public function create(StoreFormRequest $request): JsonResponse
{
$validated = $request->validated();
$validated['code'] = generate_unique_code(StoreModel::class);
$validated['password'] = password_hash((string) $validated['password'], PASSWORD_DEFAULT);
StoreModel::create($validated);
return $this->success();
}
/** 编辑门店 */
/** 编辑门店(密码留空则不修改) */
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function update(int $id, StoreFormRequest $request): JsonResponse
{
@@ -60,7 +62,13 @@ class StoreController extends BaseController
if (empty($model)) {
throw new RepositoryException('门店不存在');
}
$model->update($request->validated());
$validated = $request->validated();
if (empty($validated['password'])) {
unset($validated['password']);
} else {
$validated['password'] = password_hash((string) $validated['password'], PASSWORD_DEFAULT);
}
$model->update($validated);
return $this->success();
}
@@ -11,7 +11,6 @@ use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Models\UserModel;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
@@ -349,9 +348,9 @@ class DashboardController extends BaseController
}
/**
* 基础档案计数:在营门店 / 在售商品 / 合作供应商 / 小程序用户
* 基础档案计数:在营门店 / 在售商品 / 合作供应商
*
* @return array{stores: int, products: int, suppliers: int, users: int}
* @return array{stores: int, products: int, suppliers: int}
*/
private function archives(): array
{
@@ -359,7 +358,6 @@ class DashboardController extends BaseController
'stores' => StoreModel::query()->where('status', StoreModel::STATUS_NORMAL)->count(),
'products' => ProductModel::query()->where('status', ProductModel::STATUS_ON)->count(),
'suppliers' => SupplierModel::query()->where('status', SupplierModel::STATUS_NORMAL)->count(),
'users' => UserModel::query()->where('status', UserModel::STATUS_NORMAL)->count(),
];
}
+1 -39
View File
@@ -2,13 +2,8 @@
namespace App\Http\Controllers;
use App\Http\Requests\UserRegisterRequest;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Trait\RequestJson;
@@ -17,7 +12,7 @@ class IndexController
{
use RequestJson;
// 权限验证白名单
protected array $noPermission = ['index', 'login', 'register', 'mail'];
protected array $noPermission = ['index'];
/** 获取首页信息 */
#[GetRoute('/index')]
@@ -27,37 +22,4 @@ class IndexController
return $this->success(compact('web_setting'));
}
/** 用户登录 */
#[PostRoute('/login')]
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'username' => 'required|min:4|alphaDash',
'password' => 'required|min:4|alphaDash',
]);
if (Auth::guard('users')->attempt($credentials, true)) {
$data = $request->user('users')
->createToken($credentials['username'])
->toArray();
return $this->success($data, __('user.login_success'));
}
return $this->error(__('user.login_error'));
}
/** 用户注册 */
#[PostRoute('/register')]
public function register(UserRegisterRequest $request): JsonResponse
{
$data = $request->validated();
$model = new UserModel;
$model->username = $data['username'];
$model->password = password_hash($data['password'], PASSWORD_DEFAULT);
$model->email = $data['email'];
if ($model->save()) {
return $this->success();
}
return $this->error('创建用户失败');
}
}
+43 -80
View File
@@ -3,10 +3,7 @@
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Http\Requests\UserUpdateInfoRequest;
use App\Models\StoreModel;
use App\Models\UserModel;
use App\Services\WechatService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
@@ -15,113 +12,79 @@ use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
/**
* 小程序认证
* 小程序认证(门店 账号 + 密码 登录,不再使用微信能力)
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class AuthController extends BaseMiniController
{
/** 小程序登录 */
/** 门店登录(账号 + 密码) */
#[PostRoute('/auth/login', authorize: false)]
public function login(Request $request): JsonResponse
{
$validated = $request->validate([
'code' => 'required|string'
'username' => 'required|string|max:20',
'password' => 'required|string|max:20',
], [
'code.required' => '登录参数格式错误',
'code.string' => '登录参数格式错误',
'username.required' => '请输入登录账号',
'password.required' => '请输入登录密码',
]);
$session = app(WechatService::class)->code2Session($validated['code']);
$store = StoreModel::where('username', $validated['username'])->first();
$user = UserModel::where('openid', $session['openid'])->first();
if ($user === null || $user->status === UserModel::STATUS_DISABLED) {
return $this->error('账号不存在或已被停用');
if ($store === null || ! password_verify($validated['password'], (string) $store->password)) {
return $this->error('账号或密码错误');
}
if ($store->status === StoreModel::STATUS_DISABLED) {
return $this->error('账号已被停用,请联系客服处理');
}
$user->last_login_at = date('Y-m-d H:i:s');
$user->save();
$token = $user->createToken($user->openid)->toArray();
return $this->success([
'token' => $token['plainTextToken'],
'user' => $user->toArray(),
], __('user.login_success'));
$store->last_login_at = date('Y-m-d H:i:s');
$store->save();
}
/** 小程序注册 */
#[PostRoute('/auth/register', authorize: false)]
public function register(Request $request): JsonResponse
{
$validated = $request->validate([
'code' => 'required|string',
'phoneCode' => 'required|string',
'storeCode' => 'required|string'
], [
'code.required' => '注册参数格式错误',
'code.string' => '注册参数格式错误',
'phoneCode.required' => '注册参数格式错误',
'phoneCode.string' => '注册参数格式错误',
'storeCode.required' => '门店编码必须填写',
'storeCode.string' => '注册参数格式错误',
]);
$store = StoreModel::where('code', $validated['storeCode'])->first();
if (!$store) {
return $this->error('门店不存在!');
}
// 通过 code 换取 openid、session_key、unionid
$session = app(WechatService::class)->code2Session($validated['code']);
$user = UserModel::where('openid', $session['openid'])->first();
if ($user) {
return $this->error('你的微信已经注册,请直接登录!');
}
$userData = [
'openid' => $session['openid'],
'unionid' => $session['unionid'] ?? '',
'username' => 'wx_'.uniqid(),
'nickname' => '微信用户',
'store_id' => $store->id,
'avatar' => '',
'password' => '',
'last_login_at' => date('Y-m-d H:i:s'),
];
$phone = app(WechatService::class)->getPhone($validated['phoneCode']);
$userData['phone'] = $phone ?? '';
$user = UserModel::create($userData);
$token = $user->createToken($user->username)->toArray();
$token = $store->createToken($store->username)->toArray();
$store->load('level:id,name');
return $this->success([
'token' => $token['plainTextToken'],
'user' => $user->toArray(),
'user' => $store->toArray(),
], __('user.login_success'));
}
/** 当前用户信息 */
/** 当前门店信息(含客户等级) */
#[GetRoute('/auth/info')]
public function info(Request $request): JsonResponse
{
$user = UserModel::with(['store.level:id,name'])
->find($request->user()->id);
if ($user === null) {
$store = StoreModel::with('level:id,name')->find($request->user()->id);
if ($store === null) {
throw new RepositoryException('账号不存在');
}
return $this->success($user->toArray());
return $this->success($store->toArray());
}
#[PutRoute('auth/info')]
public function setUserInfo(UserUpdateInfoRequest $request): JsonResponse
/** 修改登录密码 */
#[PutRoute('/auth/password')]
public function setPassword(Request $request): JsonResponse
{
UserModel::where('user_id', auth('user')->id())->update($request->validated());
$data = $request->validate([
'oldPassword' => 'required|string|max:20',
'newPassword' => 'required|string|min:6|max:20',
'rePassword' => 'required|same:newPassword',
], [
'oldPassword.required' => '请输入原密码',
'newPassword.required' => '请输入新密码',
'newPassword.min' => '新密码至少 6 位',
'rePassword.same' => '两次输入的密码不一致',
]);
return $this->error('更新成功');
$store = $this->currentStore($request);
if (! password_verify($data['oldPassword'], (string) $store->password)) {
return $this->error('原密码不正确');
}
$store->password = password_hash($data['newPassword'], PASSWORD_DEFAULT);
$store->save();
return $this->success([], '密码修改成功');
}
}
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Models\StoreModel;
use App\Models\UserModel;
use Illuminate\Http\Request;
use Modules\Common\Http\Controllers\BaseController;
use Modules\SystemUser\Models\SysAccessToken;
@@ -13,71 +12,39 @@ use Modules\SystemUser\Models\SysAccessToken;
* 小程序端控制器基类
*
* 无 #[RequestAttribute],不会被 AnnoRoute 注册为路由。
* 提供当前用户获取与门店绑定前置校验。
* 门店即用户:登录主体就是门店(store 表),提供当前门店获取与状态校验。
*/
abstract class BaseMiniController extends BaseController
{
/**
* 当前小程序用户auth:sanctum 注入的 tokenable
* 当前登录门店auth:sanctum 注入的 tokenable
*/
protected function currentUser(Request $request): UserModel
protected function currentStore(Request $request): StoreModel
{
$user = UserModel::find($request->user()->id);
if ($user === null || $user->status === UserModel::STATUS_DISABLED) {
$store = StoreModel::find($request->user()->id);
if ($store === null || $store->status === StoreModel::STATUS_DISABLED) {
throw new RepositoryException('账号不存在或已被停用');
}
return $user;
return $store;
}
/**
* 可选登录场景手动识别当前用户(路由关闭 authorize 时使用)
* 可选登录场景手动识别当前门店(路由关闭 authorize 时使用)
*
* 手动解析 Bearer token;未携带 token、token 无效或非小程序用户时返回 null(不抛错)。
* 手动解析 Bearer token;未携带 token、token 无效或非门店账号时返回 null(不抛错)。
*/
protected function optionalUser(Request $request): ?UserModel
protected function optionalStore(Request $request): ?StoreModel
{
$token = $request->bearerToken();
if (empty($token)) {
return null;
}
$accessToken = SysAccessToken::findToken($token);
if ($accessToken === null || $accessToken->tokenable_type !== UserModel::class) {
if ($accessToken === null || $accessToken->tokenable_type !== StoreModel::class) {
return null;
}
$user = UserModel::find($accessToken->tokenable_id);
if ($user === null || $user->status === UserModel::STATUS_DISABLED) {
return null;
}
return $user;
}
/**
* 门店端前置校验
*/
protected function ensureStoreBound(UserModel $user): StoreModel
{
if ($user->store_id <= 0) {
throw new RepositoryException('尚未绑定门店,请联系客服处理');
}
$store = StoreModel::find($user->store_id);
if ($store === null || $store->status !== StoreModel::STATUS_NORMAL) {
throw new RepositoryException('门店不存在或已停用,请联系客服处理');
}
return $store;
}
/**
* 用户当前绑定的正常门店(未绑定/已停用返回 null,不抛错)
*/
protected function boundStore(UserModel $user): ?StoreModel
{
if ($user->store_id <= 0) {
return null;
}
$store = StoreModel::find($user->store_id);
if ($store === null || $store->status !== StoreModel::STATUS_NORMAL) {
$store = StoreModel::find($accessToken->tokenable_id);
if ($store === null || $store->status === StoreModel::STATUS_DISABLED) {
return null;
}
+3 -6
View File
@@ -43,8 +43,7 @@ class BillController extends BaseMiniController
'pageSize.max' => '每页数量最大 50',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$query = BillModel::query()
->where('store_id', $store->id)
@@ -100,8 +99,7 @@ class BillController extends BaseMiniController
'category_id' => 'nullable|integer|min:0',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string) $data['ids'])))));
if ($ids === [] || count($ids) > 100) {
@@ -131,8 +129,7 @@ class BillController extends BaseMiniController
#[GetRoute('/bill/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$bill = BillModel::with('purchase:id,purchase_no,purchase_date')
->where('store_id', $store->id)
+12 -17
View File
@@ -34,8 +34,7 @@ class CartController extends BaseMiniController
#[PostRoute('/cart')]
public function store(MiniCartRequest $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
if ($store->level_id <= 0 || $store->level === null) {
return $this->error('门店未设置客户等级,无法加购,请联系客服');
}
@@ -48,9 +47,9 @@ class CartController extends BaseMiniController
$quantity = (string) $request->validated('quantity');
$cart = DB::transaction(function () use ($user, $productId, $quantity) {
$cart = DB::transaction(function () use ($store, $productId, $quantity) {
$row = CartModel::query()
->where('user_id', $user->id)
->where('store_id', $store->id)
->where('product_id', $productId)
->lockForUpdate()
->first();
@@ -67,7 +66,7 @@ class CartController extends BaseMiniController
}
return CartModel::create([
'user_id' => $user->id,
'store_id' => $store->id,
'product_id' => $productId,
'quantity' => $quantity,
]);
@@ -85,11 +84,10 @@ class CartController extends BaseMiniController
#[GetRoute('/cart', authorize: true)]
public function index(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$rows = CartModel::query()
->where('user_id', $user->id)
->where('store_id', $store->id)
->orderBy('id', 'desc')
->get();
@@ -174,12 +172,11 @@ class CartController extends BaseMiniController
#[PutRoute('/cart/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function update(int $id, MiniCartRequest $request): JsonResponse
{
$user = $this->currentUser($request);
$this->ensureStoreBound($user);
$store = $this->currentStore($request);
$row = CartModel::query()
->where('id', $id)
->where('user_id', $user->id)
->where('store_id', $store->id)
->first();
if ($row === null) {
throw new RepositoryException('购物车项不存在');
@@ -197,12 +194,11 @@ class CartController extends BaseMiniController
#[DeleteRoute('/cart/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function destroy(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$this->ensureStoreBound($user);
$store = $this->currentStore($request);
$deleted = CartModel::query()
->where('id', $id)
->where('user_id', $user->id)
->where('store_id', $store->id)
->delete();
if ($deleted === 0) {
throw new RepositoryException('购物车项不存在');
@@ -217,10 +213,9 @@ class CartController extends BaseMiniController
#[DeleteRoute('/cart', authorize: true)]
public function clear(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$this->ensureStoreBound($user);
$store = $this->currentStore($request);
CartModel::query()->where('user_id', $user->id)->delete();
CartModel::query()->where('store_id', $store->id)->delete();
return $this->success([], '购物车已清空');
}
+16 -16
View File
@@ -11,34 +11,34 @@ use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
/**
* 小程序通知(本通知 + 全员广播)
* 小程序通知(本通知 + 全员广播)
*
* 广播已读处理:user_id=0 的广播是全局共享记录,直接改 is_read 会影响其他用户
* 因此标记已读时复制一条本专属的已读记录(data.broadcast_from 记来源),
* 广播已读处理:store_id=0 的广播是全局共享记录,直接改 is_read 会影响其他门店
* 因此标记已读时复制一条本专属的已读记录(data.broadcast_from 记来源),
* 列表查询时排除已有已读副本的广播。
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class NoticeController extends BaseMiniController
{
/** 本通知 + 全员广播(user_id in [0, 当前id]),分页 + unread_count */
/** 本通知 + 全员广播(store_id in [0, 当前id]),分页 + unread_count */
#[GetRoute('/notice', authorize: true)]
public function index(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->currentStore($request);
// 本已读过的广播来源ID(已读副本记录)
// 本已读过的广播来源ID(已读副本记录)
$readBroadcastIds = NoticeModel::query()
->where('user_id', $user->id)
->where('store_id', $store->id)
->whereNotNull('data->broadcast_from')
->pluck('data')
->map(static fn (?array $data) => $data['broadcast_from'] ?? null)
->filter()
->values();
$query = NoticeModel::query()->where(function ($q) use ($user, $readBroadcastIds) {
$q->where('user_id', $user->id)
$query = NoticeModel::query()->where(function ($q) use ($store, $readBroadcastIds) {
$q->where('store_id', $store->id)
->orWhere(function ($broadcastQuery) use ($readBroadcastIds) {
$broadcastQuery->where('user_id', NoticeModel::BROADCAST_USER_ID);
$broadcastQuery->where('store_id', NoticeModel::BROADCAST_STORE_ID);
if ($readBroadcastIds->isNotEmpty()) {
$broadcastQuery->whereNotIn('id', $readBroadcastIds->all());
}
@@ -55,24 +55,24 @@ class NoticeController extends BaseMiniController
return $this->success($data);
}
/** 标记已读(广播 → 复制本已读副本;个人通知 → 直接更新) */
/** 标记已读(广播 → 复制本已读副本;定向通知 → 直接更新) */
#[PutRoute('/notice/{id}/read', authorize: true, where: ['id' => '[0-9]+'])]
public function read(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->currentStore($request);
$notice = NoticeModel::whereIn('user_id', [NoticeModel::BROADCAST_USER_ID, $user->id])->find($id);
$notice = NoticeModel::whereIn('store_id', [NoticeModel::BROADCAST_STORE_ID, $store->id])->find($id);
if ($notice === null) {
throw new RepositoryException('通知不存在');
}
if ($notice->user_id === NoticeModel::BROADCAST_USER_ID) {
$exists = NoticeModel::where('user_id', $user->id)
if ($notice->store_id === NoticeModel::BROADCAST_STORE_ID) {
$exists = NoticeModel::where('store_id', $store->id)
->where('data->broadcast_from', $notice->id)
->exists();
if (! $exists) {
NoticeModel::create([
'user_id' => $user->id,
'store_id' => $store->id,
'type' => $notice->type,
'title' => $notice->title,
'content' => $notice->content,
+5 -10
View File
@@ -30,8 +30,7 @@ class OrderController extends BaseMiniController
#[PostRoute('/order', authorize: true)]
public function store(MiniOrderRequest $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$level = $store->level_id > 0 ? $store->level : null;
if ($level === null) {
throw new RepositoryException('门店未设置客户等级,无法下单,请联系客服');
@@ -138,8 +137,7 @@ class OrderController extends BaseMiniController
'pageSize.max' => '每页数量最大 50',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$query = StoreOrderModel::query()
->where('store_id', $store->id)
@@ -220,8 +218,7 @@ class OrderController extends BaseMiniController
throw new RepositoryException('period 参数只能是 day/week/month');
}
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
// 按数据库方言选择周期分组表达式(生产 MySQL / 测试 SQLite
$driver = DB::connection()->getDriverName();
@@ -259,8 +256,7 @@ class OrderController extends BaseMiniController
#[GetRoute('/order/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$order = StoreOrderModel::with('items')
->where('store_id', $store->id)
@@ -276,8 +272,7 @@ class OrderController extends BaseMiniController
#[PutRoute('/order/{id}/cancel', authorize: true, where: ['id' => '[0-9]+'])]
public function cancel(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$order = StoreOrderModel::where('store_id', $store->id)->find($id);
if ($order === null) {
@@ -48,8 +48,7 @@ class PaymentController extends BaseMiniController
#[GetRoute('/payment', authorize: true)]
public function index(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$query = PaymentModel::query()
->where('store_id', $store->id)
@@ -89,11 +88,10 @@ class PaymentController extends BaseMiniController
'remark.max' => '备注超过最大长度',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$billIds = array_map('intval', $data['bill_ids']);
$payment = DB::transaction(function () use ($store, $user, $data, $billIds) {
$payment = DB::transaction(function () use ($store, $data, $billIds) {
$bills = BillModel::query()
->where('store_id', $store->id)
->whereIn('id', $billIds)
@@ -119,7 +117,6 @@ class PaymentController extends BaseMiniController
$payment = PaymentModel::create([
'payment_no' => app(BillNumberService::class)->make('ZF'),
'store_id' => $store->id,
'user_id' => $user->id,
'amount' => $amount,
'pay_method' => (int) $data['pay_method'],
'voucher_ids' => array_map('intval', $data['voucher_ids']),
@@ -144,8 +141,7 @@ class PaymentController extends BaseMiniController
#[GetRoute('/payment/{id}', authorize: true, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$payment = PaymentModel::query()
->where('store_id', $store->id)
@@ -51,8 +51,7 @@ class ProductController extends BaseMiniController
->paginate($pageSize);
// 当前门店的等级(售价 = 成本价 × (100 + 等级上浮比例) / 100
$user = $this->optionalUser($request);
$store = $user !== null ? $this->boundStore($user) : null;
$store = $this->optionalStore($request);
$level = ($store !== null && $store->level_id > 0) ? $store->level : null;
$paginator->getCollection()->transform(
@@ -70,7 +69,7 @@ class ProductController extends BaseMiniController
}
/**
* 商品详情(免登录浏览;登录门店按等级上浮比例显示换算价,未登录/未绑店/未设等级 price=null
* 商品详情(免登录浏览;登录门店按等级上浮比例显示换算价,未登录/未设等级 price=null
*/
#[GetRoute('/product/{id}', authorize: false, where: ['id' => '[0-9]+'])]
public function detail(int $id, Request $request): JsonResponse
@@ -84,8 +83,7 @@ class ProductController extends BaseMiniController
}
$price = null;
$user = $this->optionalUser($request);
$store = $user !== null ? $this->boundStore($user) : null;
$store = $this->optionalStore($request);
$level = ($store !== null && $store->level_id > 0) ? $store->level : null;
if ($level !== null) {
$price = CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent);
@@ -18,8 +18,7 @@ class StoreController extends BaseMiniController
#[GetRoute('/store/info', authorize: true)]
public function info(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
return $this->success([
'id' => $store->id,
@@ -46,8 +45,7 @@ class StoreController extends BaseMiniController
'address.max' => '地址最长 255 个字符',
]);
$user = $this->currentUser($request);
$store = $this->ensureStoreBound($user);
$store = $this->currentStore($request);
$store->update($data);
@@ -26,9 +26,9 @@ class UploadController extends BaseMiniController
'file.max' => '图片不能超过 5MB',
]);
$user = $this->currentUser($request);
$store = $this->currentStore($request);
// 分组 4=用户上传,渠道 20=APP用户
$result = app(SysFileService::class)->upload($data['file'], 4, 20, $user->id);
$result = app(SysFileService::class)->upload($data['file'], 4, 20, $store->id);
return $this->success([
'id' => $result['id'],
@@ -5,8 +5,8 @@ namespace App\Http\Controllers\Order;
use App\Exceptions\RepositoryException;
use App\Models\BillModel;
use App\Models\NoticeModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
use App\Services\ItemImageResolver;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -219,26 +219,27 @@ class StoreOrderController extends BaseController
}
/**
* 状态流转后通知门店用户
* 状态流转后通知门店
*/
private function notifyStore(StoreOrderModel $order): void
{
$userIds = UserModel::query()
->where('store_id', $order->store_id)
->where('status', UserModel::STATUS_NORMAL)
->pluck('id');
$store = StoreModel::query()
->where('id', $order->store_id)
->where('status', StoreModel::STATUS_NORMAL)
->first();
if ($store === null) {
return;
}
$statusName = StoreOrderModel::STATUS_NAMES[$order->status] ?? (string) $order->status;
foreach ($userIds as $userId) {
NoticeModel::create([
'user_id' => $userId,
'type' => NoticeModel::TYPE_ORDER,
'title' => '订单状态更新',
'content' => mb_substr("您的订单 {$order->order_no} 状态已更新为「{$statusName}", 0, 500),
'data' => ['order_id' => $order->id],
'is_read' => NoticeModel::UNREAD,
]);
}
NoticeModel::create([
'store_id' => $store->id,
'type' => NoticeModel::TYPE_ORDER,
'title' => '订单状态更新',
'content' => mb_substr("您的订单 {$order->order_no} 状态更新为「{$statusName}", 0, 500),
'data' => ['order_id' => $order->id],
'is_read' => NoticeModel::UNREAD,
]);
}
/**
@@ -9,7 +9,6 @@ use App\Models\CustomerLevelModel;
use App\Models\NoticeModel;
use App\Models\ProductModel;
use App\Models\StoreModel;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -188,19 +187,14 @@ class ProductController extends BaseController
->implode('、');
$content = mb_substr('以下商品价格已调整:' . $productNames . ',下次登录小程序后按新价格显示', 0, 500);
// 成本价变更影响所有等级的售价:通知全部正常门店绑定的正常用户
$userIds = UserModel::query()
->where('status', UserModel::STATUS_NORMAL)
->whereIn('store_id', function ($q) {
$q->select('id')
->from('store')
->where('status', StoreModel::STATUS_NORMAL);
})
// 成本价变更影响所有等级的售价:通知全部正常门店
$storeIds = StoreModel::query()
->where('status', StoreModel::STATUS_NORMAL)
->pluck('id');
foreach ($userIds as $userId) {
foreach ($storeIds as $storeId) {
NoticeModel::create([
'user_id' => $userId,
'store_id' => $storeId,
'type' => NoticeModel::TYPE_PRICE,
'title' => '商品价格变更',
'content' => $content,
@@ -35,7 +35,7 @@ class PaymentController extends BaseController
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch($params, PaymentModel::query()
->with(['store:id,name', 'user:id,nickname', 'auditor:id,nickname'])
->with(['store:id,name', 'auditor:id,nickname'])
->withCount('bills'))
->orderBy('status')
->orderBy('id', 'desc')
@@ -48,7 +48,7 @@ class PaymentController extends BaseController
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
public function detail(int $id): JsonResponse
{
$payment = PaymentModel::with(['store:id,name,contact,phone', 'user:id,nickname', 'auditor:id,nickname'])->find($id);
$payment = PaymentModel::with(['store:id,name,contact,phone', 'auditor:id,nickname'])->find($id);
if (empty($payment)) {
throw new RepositoryException('支付记录不存在');
}
-68
View File
@@ -1,68 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UserUpdateInfoRequest;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\Common\Trait\RequestJson;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
#[RequestAttribute('/api/user', authGuard: 'users')]
class UserController
{
use RequestJson;
protected array $noPermission = ['refreshToken'];
#[GetRoute]
public function getUserInfo(): JsonResponse
{
$info = auth()->user();
return $this->success(compact('info'));
}
#[PostRoute('/logout')]
public function logout(): JsonResponse
{
$user_id = auth('users')->id();
$model = new UserModel;
if ($model->logout($user_id)) {
return $this->success('退出登录成功');
} else {
return $this->error($model->getErrorMsg());
}
}
#[PutRoute]
public function setUserInfo(UserUpdateInfoRequest $request): JsonResponse
{
UserModel::where('user_id', auth('user')->id())->update($request->validated());
return $this->error('更新成功');
}
#[PostRoute('/setPwd')]
public function setPassword(Request $request): JsonResponse
{
$data = $request->validate([
'oldPassword' => 'required|string|max:20',
'newPassword' => 'required|string|min:6|max:20',
'rePassword' => 'required|same:newPassword',
]);
$user_id = auth('user')->id();
$user = UserModel::query()->find($user_id);
if (! password_verify($data['oldPassword'], $user['password'])) {
return $this->error('旧密码不正确!');
}
$user->password = password_hash($data['newPassword'], PASSWORD_DEFAULT);
if ($user->save()) {
return $this->success('更新成功');
}
return $this->error('更新失败');
}
}
@@ -1,28 +0,0 @@
<?php
namespace App\Http\Requests\Customer;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 小程序用户绑定 验证(绑定门店)
*/
class MiniUserBindRequest extends BaseFormRequest
{
protected $stopOnFirstFailure = true;
public function rules(): array
{
return [
'store_id' => 'required|integer|min:1',
];
}
public function messages(): array
{
return [
'store_id.required' => '绑定门店时必须选择门店',
'store_id.min' => '门店ID不正确',
];
}
}
@@ -5,7 +5,7 @@ namespace App\Http\Requests\Customer;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 通知 创建 验证(user_id 留空 = 全员广播)
* 通知 创建 验证(store_id 留空 = 全员广播)
*/
class NoticeFormRequest extends BaseFormRequest
{
@@ -13,13 +13,13 @@ class NoticeFormRequest extends BaseFormRequest
protected function prepareForValidation(): void
{
$this->merge(['user_id' => (int) ($this->input('user_id') ?? 0)]);
$this->merge(['store_id' => (int) ($this->input('store_id') ?? 0)]);
}
public function rules(): array
{
return [
'user_id' => 'required|integer|min:0',
'store_id' => 'required|integer|min:0',
'type' => 'required|string|in:order,price,system',
'title' => 'required|string|max:100',
'content' => 'nullable|string|max:500',
@@ -6,7 +6,7 @@ use Illuminate\Validation\Rule;
use Modules\Common\Http\Requests\BaseFormRequest;
/**
* 门店 创建/编辑 验证
* 门店 创建/编辑 验证(登录账号唯一;密码创建必填,编辑留空不修改)
*/
class StoreFormRequest extends BaseFormRequest
{
@@ -14,8 +14,19 @@ class StoreFormRequest extends BaseFormRequest
public function rules(): array
{
$id = (int) $this->route('id', 0);
return [
'name' => 'required|string|max:100',
'username' => [
'required',
'string',
'min:4',
'max:20',
'alpha_dash',
Rule::unique('store', 'username')->ignore($id),
],
'password' => ($this->isMethod('post') ? 'required' : 'nullable') . '|string|min:6|max:20',
'level_id' => 'required|integer|exists:customer_level,id',
'contact' => 'nullable|string|max:50',
'phone' => 'nullable|string|max:20',
@@ -31,6 +42,14 @@ class StoreFormRequest extends BaseFormRequest
return [
'name.required' => '门店名称不能为空',
'name.max' => '门店名称最长 100 个字符',
'username.required' => '登录账号不能为空',
'username.min' => '登录账号至少 4 个字符',
'username.max' => '登录账号最长 20 个字符',
'username.alpha_dash' => '登录账号只能由字母、数字、中划线、下划线组成',
'username.unique' => '登录账号已被使用',
'password.required' => '登录密码不能为空',
'password.min' => '登录密码至少 6 位',
'password.max' => '登录密码最长 20 位',
'level_id.required' => '请选择客户等级',
'level_id.exists' => '客户等级不存在',
'payment_cycle_days.integer' => '回款周期必须为整数',
-18
View File
@@ -1,18 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserRegisterRequest extends FormRequest
{
public function rules(): array
{
return [
'username' => 'required|min:4|alphaDash',
'password' => 'required|min:4|alphaDash',
'rePassword' => 'required|min:4|same:password',
'email' => 'required|email',
];
}
}
@@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserUpdateInfoRequest extends FormRequest
{
public function rules(): array
{
return [
'username' => 'required|min:4|max:20',
'nickname' => 'required|min:4|max:20',
'gender' => 'required',
'email' => 'required|email',
'avatar_id' => 'required|integer',
'mobile' => 'required|regex:/^1[34578]\d{9}$/',
];
}
}
+6 -6
View File
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 小程序购物车模型(门店订货车:按用户归属,同商品唯一行、加购合并数量)
* 小程序购物车模型(门店订货车:按门店归属,同商品唯一行、加购合并数量)
*/
class CartModel extends Model
{
@@ -17,23 +17,23 @@ class CartModel extends Model
protected $primaryKey = 'id';
protected $fillable = [
'user_id',
'store_id',
'product_id',
'quantity',
];
protected $casts = [
'user_id' => 'integer',
'store_id' => 'integer',
'product_id' => 'integer',
'quantity' => 'decimal:2',
];
/**
* 归属用户
* 归属门店
*/
public function user(): BelongsTo
public function store(): BelongsTo
{
return $this->belongsTo(UserModel::class, 'user_id', 'id');
return $this->belongsTo(StoreModel::class, 'store_id', 'id');
}
/**
+8 -8
View File
@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 通知模型(小程序端消息:订单 / 价格变更 / 系统;user_id=0 为全员广播)
* 通知模型(小程序端消息:订单 / 价格变更 / 系统;store_id=0 为全员广播)
*/
class NoticeModel extends Model
{
@@ -22,14 +22,14 @@ class NoticeModel extends Model
/** 已读 */
public const READ = 1;
/** 全员广播时的 user_id 约定值 */
public const BROADCAST_USER_ID = 0;
/** 全员广播时的 store_id 约定值 */
public const BROADCAST_STORE_ID = 0;
protected $table = 'notice';
protected $primaryKey = 'id';
protected $fillable = [
'user_id',
'store_id',
'type',
'title',
'content',
@@ -41,15 +41,15 @@ class NoticeModel extends Model
protected $casts = [
'data' => 'array',
'is_read' => 'integer',
'user_id' => 'integer',
'store_id' => 'integer',
'read_at' => 'datetime',
];
/**
* 接收用户(user_id=0 表示全员广播,无对应用户
* 接收门店(store_id=0 表示全员广播,无对应门店
*/
public function user(): BelongsTo
public function store(): BelongsTo
{
return $this->belongsTo(UserModel::class, 'user_id', 'id');
return $this->belongsTo(StoreModel::class, 'store_id', 'id');
}
}
-10
View File
@@ -51,7 +51,6 @@ class PaymentModel extends Model
protected $fillable = [
'payment_no',
'store_id',
'user_id',
'amount',
'pay_method',
'voucher_ids',
@@ -64,7 +63,6 @@ class PaymentModel extends Model
protected $casts = [
'store_id' => 'integer',
'user_id' => 'integer',
'amount' => 'decimal:2',
'pay_method' => 'integer',
'status' => 'integer',
@@ -124,14 +122,6 @@ class PaymentModel extends Model
return $this->hasMany(BillModel::class, 'payment_id', 'id');
}
/**
* 提交人(小程序用户)
*/
public function user(): BelongsTo
{
return $this->belongsTo(UserModel::class, 'user_id', 'id');
}
/**
* 审核人(后台系统用户)
*/
+33 -12
View File
@@ -3,17 +3,19 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
/**
* 门店模型(小程序下单主体)
* 门店模型(小程序下单主体,即客户;门店即用户,账号密码登录
*/
class StoreModel extends Model
class StoreModel extends Authenticatable
{
use SoftDeletes, HasFactory;
use HasApiTokens, SoftDeletes, HasFactory, Notifiable;
/** 状态:停用 */
public const STATUS_DISABLED = 0;
@@ -26,6 +28,10 @@ class StoreModel extends Model
protected $fillable = [
'name',
'code',
'username',
'password',
'avatar',
'last_login_at',
'level_id',
'contact',
'phone',
@@ -35,11 +41,18 @@ class StoreModel extends Model
'remark',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'level_id' => 'integer',
'payment_cycle_days' => 'integer',
'total_purchase_amount' => 'decimal:2',
'status' => 'integer',
'last_login_at' => 'datetime:Y-m-d H:i:s',
'created_at' => 'datetime:Y-m-d H:i:s',
];
/**
@@ -58,14 +71,6 @@ class StoreModel extends Model
return $this->hasMany(StoreOrderModel::class, 'store_id', 'id');
}
/**
* 绑定本门店的小程序用户
*/
public function users(): HasMany
{
return $this->hasMany(UserModel::class, 'store_id', 'id');
}
/**
* 门店账单(采购单完成后按门店生成)
*/
@@ -73,4 +78,20 @@ class StoreModel extends Model
{
return $this->hasMany(BillModel::class, 'store_id', 'id');
}
/**
* 门店购物车
*/
public function carts(): HasMany
{
return $this->hasMany(CartModel::class, 'store_id', 'id');
}
/**
* 门店通知
*/
public function notices(): HasMany
{
return $this->hasMany(NoticeModel::class, 'store_id', 'id');
}
}
-8
View File
@@ -43,12 +43,4 @@ class SupplierModel extends Model
{
return $this->hasMany(ProductModel::class, 'supplier_id', 'id');
}
/**
* 绑定本供应商的小程序用户
*/
public function users(): HasMany
{
return $this->hasMany(UserModel::class, 'supplier_id', 'id');
}
}
-68
View File
@@ -1,68 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
/**
* APP 用户模型(小程序端:门店 / 供应商用户)
*/
class UserModel extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/** 状态:停用 */
public const int STATUS_DISABLED = 0;
/** 状态:正常 */
public const int STATUS_NORMAL = 1;
protected $table = 'user';
protected $primaryKey = 'id';
protected $hidden = [
'password',
'remember_token',
];
protected $fillable = [
'username',
'email',
'password',
'nickname',
'openid',
'unionid',
'phone',
'avatar',
'store_id',
'status',
'last_login_at',
];
protected $casts = [
'email_verified_at' => 'datetime:Y-m-d H:i:s',
'last_login_at' => 'datetime:Y-m-d H:i:s',
'created_at' => 'datetime:Y-m-d H:i:s',
'store_id' => 'integer',
'status' => 'integer',
];
/**
* 关联门店
*/
public function store(): BelongsTo
{
return $this->belongsTo(StoreModel::class, 'store_id', 'id');
}
/**
* 用户通知
*/
public function notices(): HasMany
{
return $this->hasMany(NoticeModel::class, 'user_id', 'id');
}
}
-4
View File
@@ -2,7 +2,6 @@
namespace App\Providers;
use App\Services\WechatService;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Foundation\Exceptions\Handler as ExceptionsHandler;
use Illuminate\Support\ServiceProvider;
@@ -18,9 +17,6 @@ class AppServiceProvider extends ServiceProvider
{
$this->app->bind(ExceptionsHandler::class, \App\Exceptions\ExceptionsHandler::class);
// 单例:测试通过 setHttpClient() 注入 Mock 后,控制器解析到同一实例
$this->app->singleton(WechatService::class);
}
/**
-99
View File
@@ -1,99 +0,0 @@
<?php
namespace App\Services;
use App\Exceptions\RepositoryException;
use EasyWeChat\Kernel\Exceptions\HttpException;
use EasyWeChat\MiniApp\Application;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* 微信小程序服务(基于 EasyWeChat 6.x
*
* 封装 code2Session / 手机号解密;配置读取 site_config('wechatMini')
* (后台「小程序设置」面板维护,存 sys_site_config 表)。
*
* 测试策略:通过 setHttpClient() 注入 Symfony MockHttpClient 拦截微信 HTTP 调用。
*/
class WechatService
{
private ?Application $app = null;
private ?HttpClientInterface $httpClient = null;
/**
* 注入自定义 HttpClient(测试注入 MockHttpClient;注入后强制重建 Application
*/
public function setHttpClient(HttpClientInterface $httpClient): void
{
$this->httpClient = $httpClient;
$this->app = null;
}
/**
* code2Session:小程序 wx.login 的 code 换取 openid / session_key
*
* @param string $code wx.login 返回的临时登录凭证
* @return array{openid: string, session_key: string, unionid?: string}
*/
public function code2Session(string $code): array
{
try {
/** @var array{openid: string, session_key: string, unionid?: string} $session */
$session = $this->app()->getUtils()->codeToSession($code);
} catch (HttpException|TransportExceptionInterface $e) {
throw new RepositoryException('微信登录失败:' . $e->getMessage());
}
return $session;
}
/**
* 获取手机号:wx.getPhoneNumber 的 phoneCode 换取手机号
*
* @param string $phoneCode 手机号授权事件返回的动态令牌
* @return string 用户手机号
*/
public function getPhone(string $phoneCode): string
{
try {
$result = $this->app()->getUtils()->getPhoneNumber($phoneCode);
} catch (HttpException|TransportExceptionInterface $e) {
throw new RepositoryException('获取手机号失败:' . $e->getMessage());
}
$phone = (string) ($result['phone_info']['phoneNumber']
?? $result['phone_info']['purePhoneNumber']
?? '');
if ($phone === '') {
throw new RepositoryException('获取手机号失败:微信未返回有效手机号');
}
return $phone;
}
/**
* EasyWeChat 小程序应用实例(懒构建单例)
*/
protected function app(): Application
{
if ($this->app === null) {
$config = (array) site_config('wechatMini', []);
if (empty($config['appid']) || empty($config['secret'])) {
throw new RepositoryException('微信小程序尚未配置(WECHAT_MINI_APPID / WECHAT_MINI_SECRET');
}
$this->app = new Application([
'app_id' => (string) $config['appid'],
'secret' => (string) $config['secret'],
]);
if ($this->httpClient !== null) {
$this->app->setHttpClient($this->httpClient);
}
}
return $this->app;
}
}