Compare commits

...

6 Commits

Author SHA1 Message Date
liutk 7259eb5fc4 完善活动详情 2024-05-19 16:42:38 +08:00
liutk 4a3ba818ec 添加绑定手机号接口 2024-05-19 11:51:56 +08:00
liutk afc887b4f2 完善资讯和首页接口 2024-05-19 10:52:53 +08:00
liutk c83110984a 处理首页今日竞猜接口 2024-05-18 22:34:45 +08:00
liutk 1d6a2ab0b3 完善微信小程序授权 2024-05-18 19:29:22 +08:00
liutk bda8aa44c2 添加微信登录 2024-05-18 19:07:39 +08:00
39 changed files with 2072 additions and 30 deletions

View File

@ -88,7 +88,7 @@ class ActivityController extends AdminController
amis()->TextControl('name', __('admin.activities.name'))->required(true),
Components::make()->cropImageControl('cover', __('admin.activities.cover'), 1.5)->required(true),
amis()->DateRangeControl('start_at', __('admin.activities.activity_at'))->extraName('end_at')->clearable(false)->format('YYYY-MM-DD HH:mm:ss')->required(true),
amis()->TextareaControl('rules', __('admin.activities.rules'))
amis()->TextareaControl('rules', __('admin.activities.rules'))->minRows(30)
]);
}
@ -162,7 +162,7 @@ class ActivityController extends AdminController
amis()->TableColumn('home_logo', __('admin.activity_games.home_logo'))->type('image')->height('30px')->width('30px'),
amis()->TableColumn('away', __('admin.activity_games.away')),
amis()->TableColumn('away_logo', __('admin.activity_games.away_logo'))->type('image')->height('30px')->width('30px'),
amis()->TableColumn('game_at', __('admin.activity_games.game_at')),
amis()->TableColumn('game_at', __('admin.activity_games.game_at'))->type('datetime'),
amis()->TableColumn('mark', __('admin.activity_games.mark')),
amis()->TableColumn('score', __('admin.activity_games.score')),
amisMake()->Operation()->label(__('admin.actions'))->buttons([
@ -240,7 +240,7 @@ class ActivityController extends AdminController
amis()->TextControl('gift_name', __('admin.activity_gifts.name'))->required(true),
Components::make()->cropImageControl('logo', __('admin.activity_gifts.logo')),
Components::make()->sortControl('rank', __('admin.activity_gifts.rank'))->required(true),
amis()->TextareaControl('explain', __('admin.activity_gifts.explain'))
amis()->TextareaControl('explain', __('admin.activity_gifts.explain'))->minRows(6)
])
])->size('lg')
)
@ -262,7 +262,7 @@ class ActivityController extends AdminController
amis()->TextControl('name', __('admin.activity_gifts.name'))->required(true),
Components::make()->cropImageControl('logo', __('admin.activity_gifts.logo')),
Components::make()->sortControl('rank', __('admin.activity_gifts.rank'))->required(true),
amis()->TextareaControl('explain', __('admin.activity_gifts.explain'))
amis()->TextareaControl('explain', __('admin.activity_gifts.explain'))->minRows(6)
])
])->size('lg')),
amisMake()->AjaxAction()->label('删除')->level('link')

View File

@ -87,7 +87,7 @@ class ArticleController extends AdminController
amis()->Wrapper()->body([
amis()->TextControl('title', __('admin.articles.title'))->required(true),
Components::make()->parentControl(admin_url('api/keywords/tree-list?parent_name=article_category&has_owner=0'), 'category', __('admin.articles.category'), 'name', 'key'),
Components::make()->keywordsTagControl('t_ids', __('admin.articles.tags'), 'article_tag'),
// Components::make()->keywordsTagControl('t_ids', __('admin.articles.tags'), 'article_tag'),
Components::make()->cropImageControl('cover', __('admin.articles.cover')),
amis()->TextControl('source', __('admin.articles.source')),
Components::make()->sortControl('sort', __('admin.articles.sort')),

View File

@ -38,10 +38,11 @@ class UserController extends AdminController
amis()->TableColumn('avatar', __('admin.users.avatar'))->type('image')->height('50px')->width('50px')->enlargeAble(true),
amis()->TableColumn('phone', __('admin.users.phone')),
amis()->TableColumn('mini_openid', __('admin.users.mini_openid')),
amis()->TableColumn('last_login_at', __('admin.users.last_login_at')),
amis()->TableColumn('last_login_ip', __('admin.users.last_login_ip')),
amis()->TableColumn('created_at', __('admin.users.created_at'))->type('datetime')->sortable(true),
amis()->TableColumn('bind_phone_at', __('admin.users.bind_phone_at'))->type('datetime')->sortable(true),
amis()->TableColumn('last_login_at', __('admin.users.last_login_at')),
amis()->TableColumn('last_ip', __('admin.users.last_login_ip')),
Operation::make()->label(__('admin.actions'))->buttons([
$this->rowShowTypeButton('drawer', 'xl'),
])

View File

@ -4,6 +4,7 @@ namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class Handler extends ExceptionHandler
{
@ -27,4 +28,12 @@ class Handler extends ExceptionHandler
//
});
}
public function render($request, Throwable $e){
// // FirstOrFail 和 FindOrFail 异常处理
if ($e instanceof ModelNotFoundException) {
if ($request->ajax() || $request->wantsJson()) {
return response()->json(['data'=>null,'code'=>404 , 'message' => '数据未找到,或已被删除' ], 404);
}
}
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use App\Models\Activity;
use App\Models\UserActivity;
use App\Http\Resources\Api\ActivityResource;
use App\Http\Resources\Api\UserActivityResource;
class ActivityController extends ApiController
{
public function index(Request $request)
{
$activities = Activity::show()->sort()
->simplePaginate($request->query('per_page', 20));
return $this->success(['activities' => ActivityResource::collection($activities)->resolve()]);
}
public function show(Activity $activity)
{
return ActivityResource::make($activity);
}
public function rankList(Request $request)
{
$activityId = $request->input('activity_id', 0);
//获取排行表
$list = UserActivity::with('user')->where('activity_id', $activityId)->sort()->limit(30)->get();
//获取当前排名;
$userMark = UserActivity::where([
'user_id' => $request->user()->id,
'activity_id' => $activityId,
])->value('mark');
$rankNumber = null;
if($userMark){
$rankNumber = UserActivity::where('activity_id', $activityId)->where('mark', '>', $userMark)->sort()->count();
$rankNumber++;
}
return $this->success(['rank_list' => UserActivityResource::collection($list)->resolve(), 'rank_number'=>$rankNumber]);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Api;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use App\Models\Activity;
use App\Models\ActivityGame;
use App\Http\Resources\Api\ActivityGameResource;
class ActivityGameController extends ApiController
{
public function index(Request $request)
{
$activityId = $request->input('activity_id', 0);
$games = ActivityGame::with(['logs'=> function($q){
$q->where('user_id', auth('api')->user()?->id ?? 0);
}])
->where('activity_id', $activityId)->show()->sort()
->simplePaginate($request->query('per_page', 20));
return $this->success(['games'=>ActivityGameResource::collection($games)->resolve()]);
}
public function latestGame(Request $request)
{
//获取最新的活动;
$activity = Activity::show()->sort()->first();
$game = $activity?->games()->show()
->whereDate('game_at', now())
->where('game_at', '>', now())
->where('state', 1)
->orderBy('game_at', 'asc')->first();
//若已经没有最新的活动了, 则拿取当天最后一个
if(!$game){
$game = $activity?->games()->show()
->whereDate('game_at', now())
->orderBy('game_at', 'desc')->first();
}
if($game){
$game->load(['logs'=> function($q){
$q->where('user_id', auth('api')->user()?->id ?? 0);
}]);
return ActivityGameResource::make($game);
}
return $this->success();
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Api;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use App\Models\Ad;
use App\Models\Keyword;
use App\Http\Resources\Api\AdResource;
class AdController extends ApiController
{
public function index(Request $request)
{
$address = $request->input('address', '');
$ads = Ad::where('address', $address)->show()->sort()->get();
return $this->success(['ads'=>AdResource::collection($ads)->resolve()]);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Response;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class ApiController extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
public function __construct()
{
if (property_exists($this, 'serviceName')) {
$this->service = $this->serviceName::make();
}
}
public function json($data, $code = Response::HTTP_OK, $message = '')
{
$result = ['data' => $data, 'code' => $code, 'message' => $message];
return response()->json($result);
}
public function success($data = null, $message = '')
{
return $this->json($data, Response::HTTP_OK, $message);
}
public function error($message = '', $code = Response::HTTP_BAD_REQUEST, $data = null)
{
return $this->json($data, $code, $message);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Api;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use App\Models\Article;
use App\Models\Keyword;
use App\Http\Resources\Api\ArticleResource;
use App\Http\Resources\Api\ArticleCategoryResource;
class ArticleController extends ApiController
{
public function recommend(Request $request)
{
$articles = Article::show()->where('is_recommend', 1)->sort()
->simplePaginate($request->query('per_page', 20));
return $this->success(['articles'=>ArticleResource::collection($articles)->resolve()]);
}
public function category(Request $request)
{
$categories = Keyword::allChildrenOfKey('article_category')->orderBy('sort', 'asc')->get();
return $this->success(['categories'=>ArticleCategoryResource::collection($categories)->resolve()]);
}
public function index(Request $request)
{
$categoryId = $request->input('category_key', 0);
$query = Article::query();
if($categoryId > 0){
$query->where('category', $categoryId);
}
$articles = $query->show()->sort()
->simplePaginate($request->query('per_page', 20));
return $this->success(['articles'=>ArticleResource::collection($articles)->resolve()]);
}
public function show(Article $article)
{
return ArticleResource::make($article);
}
}

View File

@ -1,7 +1,66 @@
<?php
namespace App\Controllers\Api;
namespace App\Http\Controllers\Api;
class AuthController{
use Cache;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Services\Api\UserService;
use App\Http\Resources\Api\UserResource;
use App\Http\Requests\CodeRequest;
use Overtrue\LaravelWeChat\EasyWeChat;
use Illuminate\Support\Arr;
class AuthController extends ApiController
{
protected string $serviceName = UserService::class;
public function codeToSession(CodeRequest $request)
{
//绑定OPenid
$code = $request->input('code');
$app = EasyWeChat::miniApp();
$utils = $app->getUtils();
try {
$response = $utils->codeToSession($code);
}catch (\Throwable $th) {
return $this->error($th->getMessage());
}
if (Arr::get($response, 'errcode')) {
return $this->error(Arr::get($response, 'errmsg'));
}
$openid = Arr::get($response, 'openid');
$sessionKey = Arr::get($response, 'session_key');
Cache::put($openid, $sessionKey, 48*60*60);
$user = $this->service->register($openid);
$apiToken = $this->service->login($user);
return $this->success([
'user' => UserResource::make($user)->resolve(),
'api_token' => $apiToken,
'expire_in' => Carbon::parse()->addDay()->toDateTimeString()
]);
}
/**
*
*/
public function refreshToken(Request $request)
{
$user = auth('api')->user();
//撤销当前令牌
$request->user()->currentAccessToken()->delete();
//颁发新令牌
return $this->success([
'api_token' => $user->createToken(
name: 'api',
expiresAt: now()->addDay(),
)->plainTextToken,
'expire_in' => Carbon::parse()->addDay()->toDateTimeString()
]);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Api;
use Cache;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Requests\CodeRequest;
use App\Services\Api\UserService;
use App\Http\Resources\Api\UserResource;
use Overtrue\LaravelWeChat\EasyWeChat;
use Illuminate\Support\Arr;
class UserController extends ApiController
{
protected string $serviceName = UserService::class;
public function bindPhone(CodeRequest $request)
{
$user = $request->user();
if($user->phone){
return $this->error('您已绑定手机号,请勿重复绑定');
}
$code = $request->input('code');
$app = EasyWeChat::miniApp();
try {
$response = $app->getClient()->postJson('wxa/business/getuserphonenumber', [
'code' => $code
]);
}catch (\Throwable $th) {
return $this->error($th->getMessage());
}
if (Arr::get($response, 'errcode')) {
return $this->error(Arr::get($response, 'errmsg'));
}
$phone = Arr::get($response['phone_info'], 'phoneNumber');
// $phone = '17784326301';
$res = $this->service->bindPhone($user, $phone);
if($res){
return $this->success(null, '绑定成功');
}
return $this->error('绑定失败,请稍后重试');
}
public function updateUserInfo()
{
}
}

View File

@ -39,6 +39,7 @@ class Kernel extends HttpKernel
],
'api' => [
\App\Http\Middleware\AcceptHeader::class,
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AcceptHeader
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class HasBindPhone
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$user = auth('api')->user();
if(!($user && $user->phone)){
return response()->json(['data'=>[], 'code'=>Response::HTTP_FORBIDDEN, 'message'=>'请先登录']);
}
return $next($request);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Contracts\Validation\Validator;
class CodeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'code' => 'required|string|max:100',
];
}
public function messages(){
$message = [
'code.required' => 'code码必填',
];
return $message;
}
protected function failedValidation(Validator $validator){
$error = $validator->errors()->all();
throw new HttpResponseException(response()->json(['data' => null, 'code' => 400, 'message' => $error[0]]));
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Resources\Api;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Response;
class ActivityGameResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'activity_id' => $this->activity_id,
'name' => $this->name,
'home_field' => $this->home_field ??'',
'home_logo' => $this->home_logo ??'',
'away' => $this->away ??'',
'away_logo' => $this->away_logo ??'',
'game_at' => $this->game_at->format('Y-m-d H:i'),
'has_guess' => !$this->logs->isEmpty() ? ($this->logs[0]->score):false,
'state' => $this->state,
];
}
public function with($request)
{
return ['code' => Response::HTTP_OK, 'message' => ''];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Resources\Api;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Response;
class ActivityResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'cover' => $this->cover ??'',
'rules' => $this->rules ??'',
'start_at' => $this->start_at->format('Y-m-d H:i'),
'end_at' => $this->end_at->format('Y-m-d H:i'),
'state' => $this->state,
];
}
public function with($request)
{
return ['code' => Response::HTTP_OK, 'message' => ''];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Resources\Api;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Response;
class AdResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'resource' => $this->resource->resource,
'jump_type' => $this->jump_type,
'jump_config' => $this->jump_config ?? []
];
}
public function with($request)
{
return ['code' => Response::HTTP_OK, 'message' => ''];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Resources\Api;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Response;
class ArticleCategoryResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'key' => $this->key,
];
}
public function with($request)
{
return ['code' => Response::HTTP_OK, 'message' => ''];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Resources\Api;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Response;
class ArticleResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'cover' => $this->cover ?? '',
'published_at' => $this->published_at->format('Y-m-d H:i'),
'source' => $this->source ?? '',
'content' => $this->content ?? '',
];
}
public function with($request)
{
return ['code' => Response::HTTP_OK, 'message' => ''];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Resources\Api;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Response;
class UserActivityResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'nick_name' => $this->user?->nick_name ?? '',
'avatar' => $this->user?->avatar ?? '',
'join_times' => $this->join_times,
'right_times' => $this->right_times,
'mark' => $this->mark,
];
}
public function with($request)
{
return ['code' => Response::HTTP_OK, 'message' => ''];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Resources\Api;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Response;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'nick_name' => $this->nick_name ??'',
'avatar' => $this->avatar ??'',
'phone' => $this->phone ??'',
];
}
public function with($request)
{
return ['code' => Response::HTTP_OK, 'message' => ''];
}
}

View File

@ -10,6 +10,16 @@ class Activity extends Model
{
use HasFactory,Filterable;
protected $casts = [
'start_at' => 'datetime',
'end_at' => 'datetime'
];
public function scopeShow($q){
$q->where('state', '>' ,0)
->where('start_at', '<', now());
}
public function scopeSort($q)
{
$q->orderBy('state', 'asc')
@ -17,6 +27,11 @@ class Activity extends Model
->orderBy('created_at', 'desc');
}
public function games()
{
return $this->hasMany(ActivityGame::class, 'activity_id');
}
public function gifts()
{
return $this->hasMany(ActivityGift::class, 'activity_id');

View File

@ -10,10 +10,19 @@ class ActivityGame extends Model
{
use HasFactory,Filterable;
protected $casts = [
'game_at' => 'datetime'
];
protected $fillable = [
'state', 'score',
];
public function scopeShow($q)
{
$q->where('state', '>' ,0);
}
public function scopeSort($q)
{
$q->orderBy('game_at', 'desc');

View File

@ -72,8 +72,8 @@ class Ad extends Model
];
}
public function scopeShow(){
$q->where('is_enable', true)->where('published_at', '>=', now());
public function scopeShow($q){
$q->where('is_enable', true)->where('published_at', '<=', now());
}
public function scopeSort($q)

View File

@ -21,9 +21,7 @@ class Article extends Model
protected $appends = ['tags'];
protected $casts = [
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
'published_at' => 'datetime:Y-m-d H:i:s',
'published_at' => 'datetime',
'is_enable' => 'boolean',
'is_recommend' => 'boolean',
'appendixes' => 'array',
@ -43,8 +41,8 @@ class Article extends Model
'source'
];
public function scopeShow(){
$q->where('is_enable', true)->where('published_at', '>=', now());
public function scopeShow($q){
$q->where('is_enable', true)->where('published_at', '<=', now());
}
public function scopeSort($q)

View File

@ -8,11 +8,13 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable, Filterable;
protected $appends = ['last_ip'];
/**
* The attributes that are mass assignable.
*
@ -25,6 +27,16 @@ class User extends Authenticatable
'mini_openid',
'union_id',
'last_login_at',
'last_login_ip'
'last_login_ip',
'bind_phone_at'
];
//ip
protected function lastIp():Attribute
{
return Attribute::make(
get: fn($value) => $this->last_login_ip ? long2ip($this->last_login_ip) : "未知",
);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Models;
use DB;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -16,7 +17,12 @@ class UserActivity extends Model
public function scopeSort($q)
{
$q->orderBy('mark', 'desc')
->orderBy('right_time', 'desc')
->orderBy('right_times', 'desc')
->orderBy('last_join_at', 'asc');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}

View File

@ -26,7 +26,7 @@ class AdService extends BaseService
$columns = $this->getTableColumns();
$model = $this->getModel();
$isEnable = Arr::get($data, 'is_enabled');
$isEnable = Arr::get($data, 'is_enable');
$publishedAt = Arr::get($data, 'published_at');
if ($isEnable && empty($publishedAt)) {
$data['published_at'] = now();

View File

@ -26,7 +26,7 @@ class ArticleService extends BaseService
$columns = $this->getTableColumns();
$model = $this->getModel();
$isEnable = Arr::get($data, 'is_enabled');
$isEnable = Arr::get($data, 'is_enable');
$publishedAt = Arr::get($data, 'published_at');
if ($isEnable && empty($publishedAt)) {
$data['published_at'] = now();

View File

@ -0,0 +1,59 @@
<?php
namespace App\Services\Api;
use App\Models\User;
use Illuminate\Http\Response;
/**
* @method User getModel()
* @method User|\Illuminate\Database\Query\Builder query()
*/
class UserService
{
public static function make(): static
{
return new static;
}
public function register($miniOpenid)
{
if(! $user = User::where('mini_openid', $miniOpenid)->first()){
$user = new User();
$user->mini_openid = $miniOpenid;
$user->save();
}
return $user;
}
public function login(User $user)
{
//记录当前登录时间、IP
$ip = request()->getClientIp();
$user->last_login_ip = ip2long($ip);
$user->last_login_at = now();
$user->save();
//撤销当前所有令牌;
$user->tokens()->delete();
return $user->createToken(
name: 'api',
expiresAt: now()->addDay(),
)->plainTextToken;
}
public function bindPhone(User $user, $phone)
{
if(User::where('phone', $phone)->where('id', '<>', $user->id)->exists()){
return response()->json(['data'=>[], 'code'=> Response::HTTP_BAD_REQUEST, 'message' => '该手机号已被其他微信号绑定,请更换手机号绑定']);
}
return $user->update([
'phone' => $phone,
'bind_phone_at' => now()
]);
}
}

View File

@ -11,6 +11,7 @@
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8",
"overtrue/laravel-wechat": "^7.2",
"rap2hpoutre/fast-excel": "^5.4",
"slowlyo/owl-admin": "^3.5",
"tucker-eric/eloquentfilter": "^3.2"

1070
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,11 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'sanctum',
'provider' => 'users',
],
],
/*

View File

@ -0,0 +1,168 @@
<?php
return [
/*
* 默认配置,将会合并到各模块中
*/
'defaults' => [
'http' => [
'timeout' => 5.0,
],
],
/*
* 公众号
*/
// 'official_account' => [
// 'default' => [
// 'app_id' => env('WECHAT_OFFICIAL_ACCOUNT_APPID', ''), // AppID
// 'secret' => env('WECHAT_OFFICIAL_ACCOUNT_SECRET', ''), // AppSecret
// 'token' => env('WECHAT_OFFICIAL_ACCOUNT_TOKEN', ''), // Token
// 'aes_key' => env('WECHAT_OFFICIAL_ACCOUNT_AES_KEY', ''), // EncodingAESKey
/*
* OAuth 配置
*
* scopes公众平台snsapi_userinfo / snsapi_base开放平台snsapi_login
* callbackOAuth授权完成后的回调页地址(如果使用中间件,则随便填写。。。)
* enforce_https是否强制使用 HTTPS 跳转
*/
// 'oauth' => [
// 'scopes' => array_map('trim', explode(',', env('WECHAT_OFFICIAL_ACCOUNT_OAUTH_SCOPES', 'snsapi_userinfo'))),
// 'callback' => env('WECHAT_OFFICIAL_ACCOUNT_OAUTH_CALLBACK', '/examples/oauth_callback.php'),
// 'enforce_https' => true,
// ],
/**
* 接口请求相关配置,超时时间等,具体可用参数请参考:
* https://github.com/symfony/symfony/blob/6.0/src/Symfony/Contracts/HttpClient/HttpClientInterface.php#L26
*/
//'http' => [
// 'timeout' => 5.0,
// // 如果你在国外想要覆盖默认的 url 的时候才使用,根据不同的模块配置不同的 uri
// 'base_uri' => 'https://api.weixin.qq.com/',
//],
// ],
// ],
/*
* 开放平台第三方平台
*/
// 'open_platform' => [
// 'default' => [
// 'app_id' => env('WECHAT_OPEN_PLATFORM_APPID', ''),
// 'secret' => env('WECHAT_OPEN_PLATFORM_SECRET', ''),
// 'token' => env('WECHAT_OPEN_PLATFORM_TOKEN', ''),
// 'aes_key' => env('WECHAT_OPEN_PLATFORM_AES_KEY', ''),
/**
* 接口请求相关配置,超时时间等,具体可用参数请参考:
* https://github.com/symfony/symfony/blob/6.0/src/Symfony/Contracts/HttpClient/HttpClientInterface.php#L26
*/
// 'http' => [
// 'timeout' => 5.0,
// // 如果你在国外想要覆盖默认的 url 的时候才使用,根据不同的模块配置不同的 uri
// 'base_uri' => 'https://api.weixin.qq.com/',
// ],
// ],
// ],
/*
* 小程序
*/
'mini_app' => [
'default' => [
'app_id' => env('WECHAT_MINI_APP_APPID', ''),
'secret' => env('WECHAT_MINI_APP_SECRET', ''),
'token' => env('WECHAT_MINI_APP_TOKEN', ''),
'aes_key' => env('WECHAT_MINI_APP_AES_KEY', ''),
/**
* 接口请求相关配置,超时时间等,具体可用参数请参考:
* https://github.com/symfony/symfony/blob/6.0/src/Symfony/Contracts/HttpClient/HttpClientInterface.php#L26
*/
'http' => [
'timeout' => 5.0,
// 如果你在国外想要覆盖默认的 url 的时候才使用,根据不同的模块配置不同的 uri
'base_uri' => 'https://api.weixin.qq.com/',
],
],
],
/*
* 微信支付
*/
// 'pay' => [
// 'default' => [
// 'app_id' => env('WECHAT_PAY_APPID', ''),
// 'mch_id' => env('WECHAT_PAY_MCH_ID', 'your-mch-id'),
// 'private_key' => '/data/private/certs/apiclient_key.pem',
// 'certificate' => '/data/private/certs/apiclient_cert.pem',
// 'notify_url' => 'http://example.com/payments/wechat-notify', // 默认支付结果通知地址
// /**
// * 证书序列号,可通过命令从证书获取:
// * `openssl x509 -in application_cert.pem -noout -serial`
// */
// 'certificate_serial_no' => '6F2BADBE1738B07EE45C6A85C5F86EE343CAABC3',
//
// 'http' => [
// 'base_uri' => 'https://api.mch.weixin.qq.com/',
// ],
//
// // v2 API 秘钥
// //'v2_secret_key' => '26db3e15cfedb44abfbb5fe94fxxxxx',
//
// // v3 API 秘钥
// //'secret_key' => '43A03299A3C3FED3D8CE7B820Fxxxxx',
//
// // 注意 此处为微信支付平台证书 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml
// 'platform_certs' => [
// '/data/private/certs/platform_key.pem',
// ],
// ],
// ],
/*
* 企业微信
*/
// 'work' => [
// 'default' => [
// 'corp_id' => env('WECHAT_WORK_CORP_ID', ''),
// 'secret' => env('WECHAT_WORK_SECRET', ''),
// 'token' => env('WECHAT_WORK_TOKEN', ''),
// 'aes_key' => env('WECHAT_WORK_AES_KEY', ''),
/**
* 接口请求相关配置,超时时间等,具体可用参数请参考:
* https://github.com/symfony/symfony/blob/6.0/src/Symfony/Contracts/HttpClient/HttpClientInterface.php#L26
*/
// 'http' => [
// 'timeout' => 5.0,
// // 如果你在国外想要覆盖默认的 url 的时候才使用,根据不同的模块配置不同的 uri
// 'base_uri' => 'https://api.weixin.qq.com/',
// ],
// ],
// ],
/*
* 企业微信开放平台
*/
// 'open_work' => [
// 'default' => [
// 'corp_id' => env('WECHAT_OPEN_WORK_CORP_ID', ''),
// 'provider_secret' => env('WECHAT_OPEN_WORK_SECRET', ''),
// 'token' => env('WECHAT_OPEN_WORK_TOKEN', ''),
// 'aes_key' => env('WECHAT_OPEN_WORK_AES_KEY', ''),
/**
* 接口请求相关配置,超时时间等,具体可用参数请参考:
* https://github.com/symfony/symfony/blob/6.0/src/Symfony/Contracts/HttpClient/HttpClientInterface.php#L26
*/
// 'http' => [
// 'timeout' => 5.0,
// // 如果你在国外想要覆盖默认的 url 的时候才使用,根据不同的模块配置不同的 uri
// 'base_uri' => 'https://api.weixin.qq.com/',
// ],
// ],
// ],
];

View File

@ -13,16 +13,19 @@ return new class extends Migration
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('mini_openid')->comment('小程序openid');
$table->string('union_id')->nullable()->default('')->comment('微信唯一标识');
$table->string('mini_openid')->unique()->comment('小程序openid');
$table->string('union_id')->nullable()->comment('微信唯一标识');
$table->string('nick_name')->nullable()->default('')->comment('微信昵称');
$table->string('avatar')->nullable()->default('')->comment('微信头像');
$table->string('phone')->nullable()->default('')->comment('手机号');
$table->timestamp('bind_phone_at')->nullable()->comment('绑定手机号时间');
$table->timestamp('last_login_at')->nullable()->comment('上次登录时间');
$table->timestamp('last_login_ip')->nullable()->comment('上次登录ip');
$table->bigInteger('last_login_ip')->nullable()->comment('上次登录ip');
$table->rememberToken();
$table->timestamps();
});
}

View File

@ -16,8 +16,8 @@ return new class extends Migration
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('activity_id');
$table->unsignedInteger('mark')->default(0)->comment('分数');
$table->unsignedInteger('join_time')->default(0)->comment('竞猜次数');
$table->unsignedInteger('right_time')->default(0)->comment('猜对次数');
$table->unsignedInteger('join_times')->default(0)->comment('竞猜次数');
$table->unsignedInteger('right_times')->default(0)->comment('猜对次数');
$table->timestamp('last_join_at')->comment('最后一次竞猜时间');
$table->unsignedTinyInteger('has_gift')->default(0)->comment('是否获奖');
$table->timestamps();

View File

@ -265,6 +265,19 @@ return [
'selected_rows_no_data' => '请选择要导出的数据',
'please_install_laravel_excel' => '请先安装 laravel-excel 扩展',
],
'components' => [
'parent_select' => '父级',
'order' => '排序',
'decimal' => '金额',
'content' => '内容',
'status' => '状态',
'status_map' => [
'enabled' => '开启',
'disabled' => '关闭'
],
'files' => '文件',
'tag' => "标签"
],
'keywords' => [
'search_name' => '名称/KEY',
'parent_keyword' => '父级关键字',
@ -310,6 +323,7 @@ return [
'avatar' => '头像',
'phone' => '手机号',
'mini_openid' => '小程序唯一标识',
'bind_phone_at' => '绑定手机时间',
'last_login_at' => '上次登录时间',
'last_login_ip' => '上次登录IP',
'created_at' => '首次进入时间',

View File

@ -2,6 +2,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\HasBindPhone;
/*
|--------------------------------------------------------------------------
@ -13,7 +14,38 @@ use Illuminate\Support\Facades\Route;
| be assigned to the "api" middleware group. Make something great!
|
*/
Route::group(['prefix' => 'miniprogram', 'namespace' => 'Api\Miniprogram'], function () {
// code2session
Route::post('code-to-session', [App\Http\Controllers\Api\AuthController::class, 'codeToSession']);
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
Route::middleware('auth:sanctum')->group(function(){
// 令牌刷新
Route::post('refresh-token', [App\Http\Controllers\Api\AuthController::class, 'refreshToken']);
// 首页接口
Route::get('latest-game', [App\Http\Controllers\Api\ActivityGameController::class, 'latestGame']);
Route::get('recommend-articles', [App\Http\Controllers\Api\ArticleController::class, 'recommend']);
Route::get('ads', [App\Http\Controllers\Api\AdController::class, 'index']);
// 资讯接口
Route::get('categories', [App\Http\Controllers\Api\ArticleController::class, 'category']);
Route::get('articles', [App\Http\Controllers\Api\ArticleController::class, 'index']);
Route::get('articles/{article}', [App\Http\Controllers\Api\ArticleController::class, 'show']);
// 活动接口
Route::get('activities', [App\Http\Controllers\Api\ActivityController::class, 'index']);
Route::get('activities/rank-list', [App\Http\Controllers\Api\ActivityController::class, 'rankList']);
Route::get('activities/{activity}', [App\Http\Controllers\Api\ActivityController::class, 'show']);
Route::get('games', [App\Http\Controllers\Api\ActivityGameController::class, 'index']);
// 绑定接口
Route::put('users/bind-phone', [App\Http\Controllers\Api\UserController::class, 'bindPhone']);
// 已授权绑定手机号
Route::middleware([HasBindPhone::class])->group(function(){
//参与竞猜
//更新资料
//竞猜记录
//中奖记录
//领奖
//中奖记录详情;
});
});
});