chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\member\controller;
use think\captcha\facade\Captcha;
use think\facade\Filesystem;
use think\facade\Request;
use ywxapp\controller\MemberBase;
/**
* Ajax 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Ajax extends MemberBase
{
/**
* Summary of needLogin
* @var array
*/
protected $noNeedLogin = ['verify'];
/**
* Summary of needRight
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize(){}
/**
* 验证码
*/
public function verify()
{
ob_clean();
return Captcha::create();
}
}
+231
View File
@@ -0,0 +1,231 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\member\controller;
use think\facade\Db;
use think\Request;
use ywxapp\controller\MemberBase;
use addon\articles\model\Article as ArticleModel;
use addon\articles\model\ArticleCategory as CategoryModel;
/**
* Article 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Article extends MemberBase
{
/**
* Summary of needLogin
* @var array
*/
protected $noNeedLogin = [];
/**
* Summary of needRight
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{
$this->model = new ArticleModel();
}
/**
* 显示资源列表
*
* @return \think\Response
*/
public function index($page = 1, $limit = 15)
{
$param = $this->request->param();
$user = $this->user();
//$this->result->success($user,$user->uid);
$query = ArticleModel::with(['category'])
->where('uid', $user->uid)
->order('create_at', 'desc');
if (! empty($param['title'])) {
$query->whereLike('title', "%{$param['title']}%");
}
if (! empty($param['cid'])) {
$query->where('cid', $param['cid']);
}
if (isset($param['status'])) {
$query->where('status', $param['status']);
}
$list = $query->page((int) $page, (int) $limit)->select()->toArray();
$count = $query->count();
return json([
'code' => 0,
'msg' => 'success',
'count' => $count,
'data' => $list,
]);
}
/**
* 显示创建资源表单页.
*
* @return \think\Response
*/
public function create()
{
try {
$user = app()->auth->info;
$data = ArticleModel::create([
'title' => '未命名草稿',
'uid' => $user->uid,
'content' => '',
'status' => 0, // 草稿状态
'create_at' => time(),
'update_at' => time(),
]);
$cates = CategoryModel::select();
$this->result->success([
'category' => $cates,
'info' => $data,
], '草稿创建成功');
} catch (\Exception $e) {
// 记录日志,但不要因为入库失败而中断上传流程(或者根据需求决定)
\think\facade\Log::error($e->getMessage());
}
}
/**
* 保存新建的资源
*
* @param \think\Request $request
* @return \think\Response
*/
public function save(Request $request)
{
if ($this->request->isAjax() && $this->request->isPut()) {
$param = $this->request->param();
Db::startTrans();
try {
$info = $this->model->findOrEmpty($param['id']);
if ($info->isEmpty()) {
$this->result->error("数据获取错误!");
}
$info->save($param);
Db::commit();
$this->result->success($info, '数据修改成功!');
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
$this->result->error('数据修改失败: ' . $e->getMessage());
}
}
}
/**
* 显示指定的资源
*
* @param int $id
* @return \think\Response
*/
public function read($id)
{
//
}
/**
* 显示编辑资源表单页.
*
* @param int $id
* @return \think\Response
*/
public function edit($id = null)
{
if ($this->request->isAjax()) {
$info = $this->model->findOrEmpty($id);
if ($info->isEmpty()) {
$this->result->error("数据获取错误!");
}
$cates = CategoryModel::select();
$this->result->success([
'category' => $cates,
'info' => $info,
], '数据创建成功');
# code...
}
}
/**
* 保存更新的资源
*
* @param int $id
* @return \think\Response
*/
public function update()
{
if ($this->request->isAjax() && $this->request->isPut()) {
$param = $this->request->param();
Db::startTrans();
try {
$info = $this->model->findOrEmpty($param['id']);
if ($info->isEmpty()) {
$this->result->error("数据获取错误!");
}
$info->save($param);
Db::commit();
$this->result->success($info, '数据修改成功!');
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
$this->result->error('数据修改失败: ' . $e->getMessage());
}
}
}
/**
* 删除数据
*
* @param \think\Request $request
* @return \think\Response
*/
public function delete()
{
if ($this->request->isAjax() && $this->request->isDelete()) {
$ids = $this->request->param('ids', '');
$force = $this->request->param('force', false);
if (empty($ids)) {
$this->result->error('请选择要删除的数据');
}
try {
Db::transaction(function () use ($ids, $force) {
if ($force) {
ArticleModel::onlyTrashed()->whereIn('id', $ids)->select()->each(function ($item) {
$item->force()->delete();
});
} else {
ArticleModel::destroy($ids);
}
});
$this->result->success();
} catch (\think\exception\HttpResponseException $e) {
throw $e;
} catch (\Throwable $th) {
$this->result->error('删除失败: ' . $th->getMessage());
}
}
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\member\controller;
use think\facade\Db;
use think\facade\Log;
use ywxapp\controller\MemberBase;
use ywxapp\model\Card as CardModel;
use app\member\validate\Card as CardValidate;
/**
* 会员卡密充值(充值中心 -> 卡密充值)
*
* 业务闭环:
* 1) 会员在「卡密充值」页输入卡号 + 密码
* 2) redeem() 校验:卡密存在 / status=0或1(未用)/ 未删除
* 3) 事务内:钱包余额 += 面值、累计充值 += 面值;user_bill 记充值流水;卡密标记 status=2、use_time=now、绑定 uid
* 4) 一次性用完即焚,重复兑换同一卡密会提示已使用
*/
class Card extends MemberBase
{
protected $noNeedLogin = [];
protected $noNeedVerify = ['*'];
protected function initialize()
{
// 卡密表自愈(补齐 batch_no 等扩展列)
\ywxapp\model\Card::ensureSchema();
}
/**
* 卡密充值页
*/
public function exchange()
{
return $this->view->fetch();
}
/**
* 兑换卡密(Ajax POST
*/
public function redeem()
{
if (!($this->request->isAjax() && $this->request->isPost())) {
$this->result->error('访问错误');
}
$user = $this->auth->model;
if (empty($user)) {
$this->result->error('请先登录', 401);
}
$post = $this->request->only(['cardno', 'password'], 'post');
$validate = new CardValidate();
if (!$validate->scene('redeem')->check($post)) {
$this->result->error($validate->getError());
}
try {
$result = CardModel::redeem((int)$user->uid, trim($post['cardno']), trim($post['password']));
} catch (\Throwable $e) {
Log::error('[Card] redeem failed: ' . $e->getMessage());
$this->result->error('兑换失败:' . $e->getMessage());
}
if ($result['success']) {
$this->result->success($result['data'] ?? [], $result['msg']);
}
$this->result->error($result['msg']);
}
/**
* 我的兑换记录(Ajax GET,会员中心内页)
*/
public function records()
{
$user = $this->auth->model;
if (empty($user)) {
$this->result->error('请先登录', 401);
}
$page = $this->request->param('page/d', 1);
$limit = $this->request->param('limit/d', 15);
if ($this->request->isAjax()) {
$list = CardModel::where('uid', (int)$user->uid)
->where('status', CardModel::STATUS_USED)
->where('delete_at', 0)
->order('use_time', 'desc')
->paginate(['page' => $page, 'list_rows' => $limit]);
$items = $list->items();
foreach ($items as &$row) {
$row['use_time_text'] = $row['use_time'] > 0 ? date('Y-m-d H:i:s', $row['use_time']) : '';
}
unset($row);
$this->result->setCount($list->total())->success($items);
}
return $this->view->fetch();
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\member\controller;
use think\Request;
use ywxapp\controller\MemberBase;
use ywxapp\controller\BaseController;
use addon\articles\model\ArticleCategory as CategoryModel;
/**
* Category 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Category extends MemberBase
{
/**
* Summary of noNeedLogin
* @var array
*/
protected $noNeedLogin = [];
/**
* Summary of needRight
* @var array
*/
protected $needRight = ['*'];
/**
* 控制器初始化
*/
protected function initialize()
{}
/**
* 显示资源列表
*
* @return \think\Response
*/
public function index()
{
}
/**
* 显示创建资源表单页.
*
* @return \think\Response
*/
public function create()
{
//
}
/**
* 保存新建的资源
*
* @param \think\Request $request
* @return \think\Response
*/
public function save(Request $request)
{
//
}
/**
* 显示指定的资源
*
* @param int $id
* @return \think\Response
*/
public function read($id)
{
//
}
/**
* 显示编辑资源表单页.
*
* @param int $id
* @return \think\Response
*/
public function edit($id = null)
{
//
}
/**
* 保存更新的资源
*
* @param \think\Request $request
* @param int $id
* @return \think\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* 删除指定资源
*
* @param int $id
* @return \think\Response
*/
public function delete($id)
{
//
}
public function tabtree()
{
$data = CategoryModel::select();
$tree = CategoryModel::tabTree($data);
$this->result->success($tree);
}
}
+203
View File
@@ -0,0 +1,203 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\member\controller;
use ywxapp\controller\MemberBase;
/**
* Index 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Index extends MemberBase
{
/**
* Summary of needLogin
* @var array
*/
protected $noNeedLogin = [];
/**
* Summary of needRight
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize(){}
/**
* 显示资源列表
*
* @return \think\Response
*/
public function index()
{
// 会员中心主框架为完整 iframe 页面,不需要套用全局 layoutcommon/layout
$this->view->layout(false);
$arrayVar = [
"logo" => [
"title" => "Wxapp Mmember",
"src" => "static/images/logo.png",
],
"menu" => [
"url" => "/user/index/menu",
],
"tabs" => [
"home" => [
"id" => "1",
"href" => "console/index",
"name" => "console:index",
"title" => "首页",
],
],
'container' => 'WXapp', // 容器 ID
'entry' => 'index', // 默认视图文件名
'route_prefix' => '/user/views/',
'engine' => 'html', // 视图文件后缀名
'name' => 'AdminUI', // 系统名称
'theme' => [
'color' => [
['main' => '#20222A', 'selected' => '#16baaa', 'alias' => 'default'],
['main' => '#03152A', 'selected' => '#3B91FF', 'alias' => 'dark-blue'],
['main' => '#2E241B', 'selected' => '#A48566', 'alias' => 'coffee'],
['main' => '#50314F', 'selected' => '#7A4D7B', 'alias' => 'purple-red'],
['main' => '#344058', 'logo' => '#1E9FFF', 'selected' => '#1E9FFF', 'alias' => 'ocean'],
['main' => '#3A3D49', 'logo' => '#2F9688', 'selected' => '#16b777', 'alias' => 'green'],
['main' => '#20222A', 'logo' => '#F78400', 'selected' => '#F78400', 'alias' => 'red'],
['main' => '#28333E', 'logo' => '#AA3130', 'selected' => '#AA3130', 'alias' => 'fashion-red'],
['main' => '#24262F', 'logo' => '#3A3D49', 'selected' => '#16baaa', 'alias' => 'classic-black'],
['logo' => '#226A62', 'header' => '#2F9688', 'alias' => 'green-header'],
['main' => '#344058', 'logo' => '#0085E8', 'selected' => '#1E9FFF', 'header' => '#1E9FFF', 'alias' => 'ocean-header'],
['header' => '#393D49', 'alias' => 'classic-black-header'],
['main' => '#50314F', 'logo' => '#50314F', 'selected' => '#7A4D7B', 'header' => '#50314F', 'alias' => 'purple-red-header'],
['main' => '#28333E', 'logo' => '#28333E', 'selected' => '#AA3130', 'header' => '#AA3130', 'alias' => 'fashion-red-header'],
['main' => '#28333E', 'logo' => '#16baaa', 'selected' => '#16baaa', 'header' => '#16baaa', 'alias' => 'green-header'],
['main' => '#393D49', 'logo' => '#393D49', 'selected' => '#16baaa', 'header' => '#23262E', 'alias' => 'Classic-style1'],
['main' => '#001529', 'logo' => '#001529', 'selected' => '#1890FF', 'header' => '#1890FF', 'alias' => 'Classic-style2'],
['main' => '#25282A', 'logo' => '#25282A', 'selected' => '#35BDB2', 'header' => '#35BDB2', 'alias' => 'Classic-style3'],
],
'initColorIndex' => 3, // 初始颜色索引
],
"other" => ["keepLoad" => "1200", "autoHead" => false, "footer" => false],
];
$this->view->assign($arrayVar);
// 注入当前登录用户,供右上角头像/昵称显示
// 未登录时 $this->auth->model 为 null,需整体降级,避免 null->avatar 致命错误
$authUser = $this->auth->model;
$this->view->assign('member', [
'nickname' => $authUser->nickname ?? '会员',
'avatar' => $authUser->avatar ?? '/static/common/images/avatar.jpg',
'account' => $authUser->account ?? '',
]);
return $this->view->fetch();
}
/**
* 菜单数据
*
* 会员中心菜单:仅指向会员端真实可用页面(避免后台路由导致 403)。
* 结构使用 child 键,供前端 buildSideMenu 递归渲染;route 为 iframe 内打开的地址。
*/
public function menu()
{
$tree = [
[
'name' => 'console',
'title' => '控制台',
'icon' => 'layui-icon-home',
'route' => '/static/member/views/console/index.html',
'child' => [],
],
[
'name' => 'task',
'title' => '任务中心',
'icon' => 'layui-icon-rate',
'route' => '/user/task/index',
'child' => [],
],
[
'name' => 'myprop',
'title' => '我的道具',
'icon' => 'layui-icon-gift',
'route' => '/user/task/myProp',
'child' => [],
],
[
'name' => 'mymedal',
'title' => '我的勋章',
'icon' => 'layui-icon-trophy',
'route' => '/user/medal/index',
'child' => [],
],
[
'name' => 'payment',
'title' => '充值中心',
'icon' => 'layui-icon-rmb',
'route' => '#',
'spread' => false,
'child' => [
[
'name' => 'recharge',
'title' => '套餐充值',
'icon' => '',
'route' => '/user/payment/index',
'child' => [],
],
[
'name' => 'card',
'title' => '卡密充值',
'icon' => '',
'route' => '/user/card/exchange',
'child' => [],
],
[
'name' => 'cardrecord',
'title' => '兑换记录',
'icon' => '',
'route' => '/user/card/records',
'child' => [],
],
],
],
[
'name' => 'forum',
'title' => '社区中心',
'icon' => 'layui-icon-release',
'route' => '#',
'spread' => false,
'child' => [
[
'name' => 'post',
'title' => '我要发帖',
'icon' => '',
'route' => '/forum/member/forum/post',
'child' => [],
],
[
'name' => 'mytopics',
'title' => '我的帖子',
'icon' => '',
'route' => '/forum/member/forum/mytopics',
'child' => [],
],
],
],
];
$this->result->success($tree);
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\member\controller;
use app\member\validate\Login as LoginValidate;
use think\exception\ValidateException;
use think\facade\Event;
use think\facade\View;
use think\Request;
use ywxapp\controller\MemberBase;
/**
* Login 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Login extends MemberBase
{
/**
* 不需要登录验证的方法
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* 不需要权限验证的方法
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{}
/**
* 显示资源列表
*
* @param \think\Request $request
* @return \think\Response
*/
public function index()
{
$this->view->layout(false);
View::assign('title', '登录');
return View::fetch();
}
/**
* 保存新建的资源
*
* @param \think\Request $request
* @return \think\Response
*/
public function save()
{
if ($this->request->isPost()) {
$data = $this->request->param();
try {
validate(LoginValidate::class)->check($data);
$this->app->event->trigger('UserLoginBefore', $data);
$this->auth->login($data['username'], $data['password'], true);
if ($this->auth->isLogin) {
return $this->result->success();
}
} catch (ValidateException $e) {
$this->result->error($e->getMessage(), 1);
}
}
$this->result->error("请求访问错误!");
}
/**
* 退出登录
*
* @param \think\Request $request
* @return \think\Response
*/
public function logout()
{
$this->auth->logout();
$this->result->success('退出登录成功');
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\member\controller;
use ywxapp\model\Medal as MedalModel;
use ywxapp\controller\MemberBase;
/**
* 会员勋章中心
*/
class Medal extends MemberBase
{
protected $noNeedLogin = [];
protected $noNeedVerify = ['*'];
protected function initialize() {}
/**
* 我的勋章页
*/
public function index()
{
return $this->view->fetch('medal/index');
}
/**
* 我的勋章列表(AJAX)
*/
public function list()
{
$uid = (int) ($this->auth->model->uid ?? 0);
$medals = MedalModel::getUserMedals($uid);
foreach ($medals as &$m) {
$m['create_at_text'] = $m['create_at'] ? date('Y-m-d H:i', (int) $m['create_at']) : '';
}
unset($m);
$this->result->success($medals);
}
}
+209
View File
@@ -0,0 +1,209 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\member\controller;
use think\facade\Event;
use think\Request;
use ywxapp\controller\MemberBase;
use app\member\validate\Payment as PaymentValidate;
/**
* 支付控制器
*
* 设计原则:核心只负责「订单/流程编排 + 事件触发」,具体支付方式由【支付插件】实现,
* 通过监听以下事件注入能力(插件在 info.php 的 events 中声明监听器即可):
*
* - PaymentMethods 收集可用支付方式。监听器返回 array,如:
* [['code'=>'wechat','name'=>'微信支付','desc'=>'...','icon'=>'','sort'=>0], ...]
* - PaymentCreate 生成支付参数。监听器接收 ['order'=>[...],'params'=>[...]]
* 返回网关参数 array(按需返回 null 表示不匹配该方法),如:
* ['type'=>'qrcode','qrcode_url'=>'weixin://...','expire'=>600]
* ['type'=>'redirect','redirect_url'=>'https://...']
* ['type'=>'form','html'=>'<form ...>']
* - PaymentNotify 异步回调。监听器接收 ['method'=>...,'request'=>[...],'get'=>[...]]
* 完成验签与订单激活后,返回网关要求的响应字符串(如 'success')。
* - PaymentFreeActive 免费套餐开通钩子(无支付)。
* - PaymentOrderCreate 订单落库钩子(插件可在此持久化订单)。
*
* 未安装任何支付插件时,create() 会返回「暂无可用支付方式」提示。
*/
class Payment extends MemberBase
{
/**
* 套餐配置(来自 config/member.php,价格单位:元,duration 单位:天)
* @var array
*/
private $plans = [];
/**
* 支付页 / 套餐列表(免登录可看)
* @var array
*/
protected $noNeedLogin = ['index', 'methods'];
/**
* @var array
*/
protected $noNeedVerify = ['*'];
protected function initialize()
{
$this->plans = config('member.plans', []);
}
/**
* 支付页面
*/
public function index()
{
$this->view->assign('plans', array_values($this->plans));
return $this->view->fetch();
}
/**
* 获取可用支付方式(插件通过 PaymentMethods 事件注入)
*/
public function methods()
{
$results = Event::trigger('PaymentMethods', []);
$methods = [];
$codes = [];
foreach ((array)$results as $r) {
if (!is_array($r)) {
continue;
}
foreach (array_values($r) as $m) {
if (!is_array($m) || empty($m['code']) || in_array($m['code'], $codes, true)) {
continue;
}
$codes[] = $m['code'];
$methods[] = $m;
}
}
// 按 sort 排序
usort($methods, function ($a, $b) {
return ($a['sort'] ?? 0) <=> ($b['sort'] ?? 0);
});
// 兜底:若没有任何插件注入支付方式(如插件系统未初始化),至少提供「个人免签」体验方式
if (empty($methods)) {
$methods[] = [
'code' => 'personal',
'name' => '个人免签',
'desc' => '扫码转账到个人收款码,站长确认到账后开通',
'icon' => '',
'sort' => 0,
];
}
$this->result->success($methods);
}
/**
* 创建支付订单
* 1) 免费套餐:直接触发 PaymentFreeActive,无需支付
* 2) 付费套餐:触发 PaymentOrderCreate(落库钩子)+ PaymentCreate(网关参数钩子)
*/
public function create()
{
if (!($this->request->isAjax() && $this->request->isPost())) {
$this->result->error('访问错误');
}
$user = $this->auth->model;
$planId = (int)$this->request->param('plan_id', 0);
$method = (string)$this->request->param('method', '');
$validate = new PaymentValidate();
if (! $validate->check(['plan_id' => $planId, 'method' => $method])) {
$this->result->error($validate->getError());
}
$plan = $this->getPlan($planId);
if (!$plan) {
$this->result->error('套餐不存在', 1);
}
// 免费套餐直接开通
if ($plan['price'] <= 0) {
Event::trigger('PaymentFreeActive', ['member' => $user, 'plan' => $plan]);
$this->result->success(['type' => 'free', 'plan' => $plan['title']]);
}
if (!$method) {
$this->result->error('请选择支付方式', 1);
}
$order = [
'order_sn' => $this->genOrderSn($user->uid),
'uid' => $user->uid,
'plan_id' => $planId,
'plan_title' => $plan['title'],
'amount' => $plan['price'],
'method' => $method,
'create_at' => time(),
];
// 订单落库钩子(插件可持久化订单)
Event::trigger('PaymentOrderCreate', ['order' => $order]);
// 生成支付参数:由各支付插件监听 PaymentCreate 返回网关数据
$results = Event::trigger('PaymentCreate', [
'order' => $order,
'params' => $this->request->param(),
]);
$gateway = null;
foreach ((array)$results as $r) {
if (is_array($r) && !empty($r)) {
$gateway = $r;
break;
}
}
if (empty($gateway)) {
$this->result->error('暂无可用支付方式,请先安装支付插件', 1);
}
$this->result->success($gateway);
}
/**
* 异步回调(由各支付插件处理具体网关通知)
* 插件监听 PaymentNotify 完成验签与订单激活,并返回网关要求的响应体。
*/
public function notify()
{
$method = (string)$this->request->param('method', '');
$results = Event::trigger('PaymentNotify', [
'method' => $method,
'request' => $this->request->param(),
'get' => $this->request->get(),
'post' => $this->request->post(),
]);
foreach ((array)$results as $r) {
if (is_string($r) && $r !== '') {
return $r;
}
}
return 'success';
}
/**
* 根据套餐 id 获取套餐配置
*/
private function getPlan(int $id): ?array
{
return $this->plans[$id] ?? null;
}
/**
* 生成订单号
*/
private function genOrderSn(int $uid): string
{
return date('YmdHis') . $uid . mt_rand(100, 999);
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\member\controller;
use think\Request;
use ywxapp\service\FileStorageService;
use ywxapp\model\MemberProfile;
use ywxapp\model\Medal;
use app\member\validate\Profile as ProfileValidate;
use ywxapp\controller\MemberBase;
/**
* Profile 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Profile extends MemberBase
{
/**
* 不需要登录验证的方法
* @var array
*/
protected $noNeedLogin = [];
/**
* 不需要权限验证的方法
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{}
/**
* 个人资料页
*/
public function index()
{
$user = $this->auth->model;
$this->view->assign('info', $user);
$this->view->assign('profile', $user->profile ?? new MemberProfile());
$this->view->assign('medals', Medal::getUserMedals((int) $user->uid));
return $this->view->fetch();
}
/**
* 保存个人资料
*/
public function update()
{
if (! $this->request->isPost()) {
$this->result->error('请求方式错误');
}
$data = $this->request->param();
$validate = new ProfileValidate();
if (! $validate->check($data)) {
$this->result->error($validate->getError());
}
$user = $this->auth->model;
// 基础资料(用户表)
$user->nickname = $data['nickname'] ?? $user->nickname;
$user->avatar = $data['avatar'] ?? $user->avatar;
$user->email = $data['email'] ?? $user->email;
$user->mobile = $data['mobile'] ?? $user->mobile;
$user->save();
// 扩展资料(user_profile 表)
$profile = $user->profile;
if (! $profile) {
$profile = new MemberProfile();
$profile->uid = $user->uid;
}
if (isset($data['gender'])) {
$profile->gender = (int) $data['gender'];
}
if (isset($data['bio'])) {
$profile->bio = $data['bio'];
}
$profile->save();
$this->result->success('保存成功');
}
/**
* 修改密码页
*/
public function password()
{
return $this->view->fetch('profile/password');
}
/**
* 保存新密码
*/
public function passwordSave()
{
if (! $this->request->isPost()) {
$this->result->error('请求方式错误');
}
$data = $this->request->param();
$validate = new ProfileValidate();
if (! $validate->scene('password')->check($data)) {
$this->result->error($validate->getError());
}
$user = $this->auth->model;
$old = (string) $this->request->param('old_password', '');
$new = (string) $this->request->param('password', '');
$new2 = (string) $this->request->param('password_confirm', '');
if ($new === '' || $new !== $new2) {
$this->result->error('两次新密码不一致或为空');
}
if (! $user->checkPassword($old)) {
$this->result->error('原密码错误');
}
$user->resetPassword($new);
$this->result->success('密码修改成功');
}
/**
* 头像上传(裁剪后的图片)
*/
public function avatar()
{
if ($this->request->isPost()) {
$file = $this->request->file('file');
if (! $file || ! $file->isValid()) {
$this->result->error('请选择上传文件', 400);
}
$type = $this->request->param('type', 'avatar');
$storage = new FileStorageService();
$result = $storage->upload($file, $type . '/' . date('Ymd'));
$info = $this->auth->model;
if ($info) {
$info->avatar = $result['storage'] == 'local' ? '/storage/' . $result['path'] : $result['url'];
$info->save();
$this->result->success(['src' => $info->avatar], '头像上传成功');
} else {
$this->result->error($file->getError(), 500);
}
}
return $this->view->fetch();
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\member\controller;
use app\member\validate\Register as RegisterValidate;
use think\exception\ValidateException;
use think\facade\Env;
use think\facade\Event;
use think\Request;
use ywxapp\controller\MemberBase;
use ywxapp\library\Sms;
use ywxapp\model\Sms as SmsModel;
use ywxapp\model\MemberUser as UserModel;
use ywxapp\model\MemberProfile;
use ywxapp\model\MemberGroupAccess;
/**
* Register 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Register extends MemberBase
{
/**
* 无需登录即可访问注册相关接口
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* @var array
*/
protected $noNeedVerify = ['*'];
protected function initialize()
{}
/**
* 注册页
*/
public function index()
{
$this->view->layout(false);
return $this->view->fetch();
}
/**
* 发送注册短信验证码
* 事件驱动:SmsSend 由短信插件实际下发;开发环境默认监听空发并回传验证码。
*/
public function sendsms()
{
if (!($this->request->isAjax() && $this->request->isPost())) {
$this->result->error('访问错误');
}
$cellphone = (string)$this->request->param('cellphone', '');
if (!preg_match('/^1[3-9]\d{9}$/', $cellphone)) {
$this->result->error('手机号格式不正确', 1);
}
if (UserModel::where('mobile', $cellphone)->whereOr('account', $cellphone)->find()) {
$this->result->error('该手机号已注册', 1);
}
if (!Sms::send($cellphone, null, 'register')) {
$this->result->error('验证码发送失败', 1);
}
// 开发模式把验证码回传前端便于测试
if (Env::get('app_debug')) {
$code = SmsModel::where(['mobile' => $cellphone, 'event' => 'register'])
->order('id', 'DESC')->value('code');
$this->result->success(['debug_code' => $code]);
}
$this->result->success();
}
/**
* 注册提交
* 流程:校验表单 → 校验短信验证码 → 创建用户(模型自动加盐哈希密码)
* → 写入资料/用户组 → 触发 UserRegister / MemberLog 事件(插件钩子点)
*/
public function save(Request $request)
{
if (!($this->request->isAjax() && $this->request->isPost())) {
$this->result->error('访问错误');
}
$data = $this->request->param();
try {
validate(RegisterValidate::class)->check($data);
} catch (ValidateException $e) {
$this->result->error($e->getMessage(), 1);
}
// 校验短信验证码(Sms::check 比对数据库,无需监听)
if (!Sms::check($data['cellphone'], $data['vercode'], 'register')) {
$this->result->error('验证码错误或已过期', 1);
}
if (UserModel::where('mobile', $data['cellphone'])->whereOr('account', $data['cellphone'])->find()) {
$this->result->error('该手机号已注册', 1);
}
$user = UserModel::create([
'account' => $data['cellphone'],
'mobile' => $data['cellphone'],
'nickname' => $data['nickname'],
'email' => $data['cellphone'] . '@mobile.local', // 手机号注册无邮箱,写入唯一占位避免唯一键冲突
'password' => $data['password'], // 触发模型 setPasswordAttr 自动加盐哈希
'status' => 1,
'gid' => 2, // 普通会员组(wxapp_user_group.id=2
'create_ip' => $this->request->ip(),
'update_ip' => $this->request->ip(),
]);
// 建立用户资料行与默认用户组
MemberProfile::create(['uid' => $user->uid]);
MemberGroupAccess::create(['uid' => $user->uid, 'gid' => 2]);
// ===== 插件事件触发点 =====
// UserRegister:业务扩展钩子,插件可在此发欢迎信、初始化钱包、分配角色等
Event::trigger('UserRegister', ['member' => $user, 'data' => $data]);
// MemberLog:审计钩子(可监听写入 wxapp_user_log
Event::trigger('MemberLog', [
'uid' => $user->uid,
'action' => 'register',
'ip' => $this->request->ip(),
'remark' => '用户注册',
]);
Sms::flush($data['cellphone'], 'register');
$this->result->success();
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\member\controller;
use think\facade\Event;
use ywxapp\controller\MemberBase;
use ywxapp\model\MemberUser as UserModel;
use app\member\validate\Settings as SettingsValidate;
/**
* 账户设置
* 解决此前「设置」菜单为死链的问题(member 应用原无 Settings 控制器)。
* 页面展示账户基础信息,并支持更新昵称 / 邮箱,更新时触发 UserSettingUpdate 事件。
*/
class Settings extends MemberBase
{
/**
* 需登录
* @var array
*/
protected $noNeedLogin = [];
/**
* @var array
*/
protected $noNeedVerify = ['*'];
protected function initialize()
{}
/**
* 设置页
*/
public function index()
{
$user = $this->user();
$this->view->assign('info', $user);
return $this->view->fetch();
}
/**
* 保存设置
*/
public function save()
{
if (!($this->request->isAjax() && $this->request->isPost())) {
$this->result->error('访问错误');
}
$user = $this->user();
$data = $this->request->param();
$validate = new SettingsValidate();
if (! $validate->check($data)) {
$this->result->error($validate->getError());
}
$update = [];
if (isset($data['nickname']) && $data['nickname'] !== '') {
$update['nickname'] = $data['nickname'];
}
if (isset($data['email']) && $data['email'] !== '') {
$update['email'] = $data['email'];
}
if (!empty($update)) {
$user->save($update);
// 插件事件触发点:账户设置更新
Event::trigger('UserSettingUpdate', ['member' => $user, 'data' => $update]);
}
$this->result->success();
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\member\controller;
use ywxapp\model\Task as TaskModel;
use ywxapp\controller\MemberBase;
use ywxapp\model\BaseModel;
use think\facade\Db;
/**
* 会员任务中心
*/
class Task extends MemberBase
{
protected $noNeedLogin = [];
protected $noNeedVerify = ['*'];
protected function initialize() {}
/**
* 任务中心列表页
*/
public function index()
{
return $this->view->fetch();
}
/**
* 任务列表(AJAX):返回启用任务 + 当前用户已领取状态
*/
public function list()
{
$uid = (int) ($this->auth->model->uid ?? 0);
$tasks = TaskModel::getEnabledList();
$claimed = TaskModel::getClaimedTaskIds($uid);
$list = [];
foreach ($tasks as $t) {
$t = $t->toArray();
$list[] = [
'id' => $t['id'],
'title' => $t['title'],
'description' => $t['description'],
'reward_type' => $t['reward_type'],
'reward_type_text' => TaskModel::getRewardTypeText((int) $t['reward_type']),
'reward_num' => $t['reward_num'],
'claimed' => in_array($t['id'], $claimed, true),
];
}
$this->result->success($list);
}
/**
* 领取任务奖励(AJAX)
*/
public function claim()
{
if (! $this->request->isPost()) {
$this->result->error('请求方式错误');
}
$uid = (int) ($this->auth->model->uid ?? 0);
$taskId = (int) $this->request->param('task_id', 0);
if ($uid <= 0) {
$this->result->error('请先登录会员中心');
}
if ($taskId <= 0) {
$this->result->error('任务参数错误');
}
[$ok, $msg] = TaskModel::claim($uid, $taskId);
if ($ok) {
$this->result->success([], $msg);
}
$this->result->error($msg);
}
/**
* 我的道具页
*/
public function myProp()
{
return $this->view->fetch('task/myprop');
}
/**
* 我的道具列表(AJAX)
*/
public function myPropList()
{
$uid = (int) ($this->auth->model->uid ?? 0);
\ywxapp\model\Task::ensureSchema(); // 确保 user_prop 关联表已就绪
if ($uid <= 0) {
$this->result->error('请先登录');
}
$list = Db::name('member_prop')
->alias('up')
->join('prop p', 'p.id = up.prop_id', 'left')
->where('up.uid', $uid)
->field('up.id, up.prop_id, up.num, up.create_at, p.title, p.icon')
->order('up.create_at', 'desc')
->select()
->toArray();
$this->result->success($list);
}
}