初始化项目:添加后端代码、ThinkPHP框架、前端资源

This commit is contained in:
amb
2026-09-03 12:42:39 +08:00
commit 482bece22e
3726 changed files with 416708 additions and 0 deletions
@@ -0,0 +1,52 @@
<?php
namespace app\admin\model\order;
use think\Model;
class Evaluate extends Model
{
// 表名
protected $name = 'order_evaluate';
// 自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = false;
protected $deleteTime = false;
// 追加属性
protected $append = [
];
public function user()
{
return $this->belongsTo('app\admin\model\User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
}
public function item()
{
return $this->belongsTo('app\admin\model\order\Item', 'item_id', 'item_id', [], 'LEFT')->setEagerlyType(0);
}
public function course()
{
return $this->belongsTo('app\admin\model\course\Course', 'item_id', 'id', [], 'LEFT')->setEagerlyType(0);
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace app\admin\model\order;
use think\Model;
class Item extends Model
{
// 表名
protected $name = 'order_item';
// 自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = false;
protected $deleteTime = false;
// 追加属性
protected $append = [
'refund_time_text'
];
// 定义订单商品与订单的关联关系
public function order()
{
return $this->belongsTo('\app\admin\model\order\Order', 'order_no');
}
public function getGoodsTypeList()
{
return [
'activity' => __('activity'),
'composite' => __('composite'),
'course' => __('course'),
'exercises' => __('exercises'),
'vipcard' => __('vipcard')
];
}
public function getRefundTimeTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['refund_time']) ? $data['refund_time'] : '');
return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
}
protected function setRefundTimeAttr($value)
{
return $value === '' ? null : ($value && !is_numeric($value) ? strtotime($value) : $value);
}
public function detail()
{
return $this->belongsTo('app\admin\model\Course', 'item_id', 'id', [], 'LEFT')->setEagerlyType(0);
}
}
+178
View File
@@ -0,0 +1,178 @@
<?php
namespace app\admin\model\order;
use think\Model;
use app\admin\model\order\Item;
class Order extends Model
{
// 表名
protected $name = 'order';
// 自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = false;
protected $deleteTime = false;
// 追加属性
protected $append = [
// 'pay_time_text',
// 'pay_type_text',
// 'source_text',
// 'send_type_text',
// 'status_text'
];
/**
* 获取销量
* @params $goodsType course\vipcard\activity\exercises\
* @param $goodsId 商品ID
* @return void
*/
public function getSales($goodsType,$goodsId){
return \app\admin\model\order\Item::alias('item')
->join('order','item.order_no=order.order_no','LEFT')
->where([
'goods_type'=>$goodsType,
'item_id'=>$goodsId,
'order.status'=>['in',\app\common\constant\order\Status::getSuccessOrderStatus()]
])->sum('count');
}
public function getPayTypeList()
{
return ['balance' => __('Balance'), 'alipay' => __('Alipay'), 'wxpay' => __('Wxpay')];
}
public function getSourceList()
{
return ['h5' => __('source.H5'),'channels' => __('source.channels'), 'wxOfficialAccount' => __('source.wxOfficialAccount'), 'wxMiniProgram' => __('source.wxMiniProgram'), 'dyMiniProgram' => __('source.dyMiniProgram')];
}
public function getSendTypeList()
{
return ['express' => __('Express'), 'noexpress' => __('Noexpress'), 'offline' => __('Offline')];
}
public function getVirtualPayList()
{
return ['0' => __('否'), '1' => __('是')];
}
public function getTypeList()
{
$list = ['goods' => __('order_type.goods'), 'score' => __('order_type.score'), 'vipcard' => __('order_type.vipcard'), 'exercises' => __('order_type.exercises'), 'physical' => __('order_type.physical'), 'live_gift' => __('order_type.live_gift')];
// 组合商品订单类型仅在 composite 插件已安装时显示
if (\app\admin\library\project\App::isInstall('composite')) {
$list['composite'] = __('order_type.composite');
}
return $list;
}
public function getPayTimeTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['pay_time']) ? $data['pay_time'] : '');
return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
}
public function getPayTypeTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['pay_type']) ? $data['pay_type'] : '');
$list = $this->getPayTypeList();
return isset($list[$value]) ? $list[$value] : '';
}
public function getSourceTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['source']) ? $data['source'] : '');
$list = $this->getSourceList();
return isset($list[$value]) ? $list[$value] : '';
}
public function getSendTypeTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['send_type']) ? $data['send_type'] : '');
$list = $this->getSendTypeList();
return isset($list[$value]) ? $list[$value] : '';
}
public function getStatusList()
{
return ['close' => __('Close'), 'unpaid' => __('Unpaid'), 'unsend' => __('Unsend'), 'unreceive' => __('Unreceive'), 'success' => __('Success'), 'cancel' => __('Cancel'), 'after_sale' => __('After_sale'), 'refund' => __('Refund')];
}
public function getStatusTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['status']) ? $data['status'] : '');
$list = $this->getStatusList();
return isset($list[$value]) ? $list[$value] : '';
}
protected function setPayTimeAttr($value)
{
return $value === '' ? null : ($value && !is_numeric($value) ? strtotime($value) : $value);
}
public function user()
{
return $this->belongsTo('app\admin\model\User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
}
public function item()
{
return $this->hasMany('app\admin\model\order\Item', 'order_no', 'order_no');
}
public function log()
{
return $this->hasMany('app\common\model\order\Log','order_no', 'order_no');
}
/**
* 获取交易统计
* @return void
*/
public function getTransactionStatistic($userId){
$data = self::where([
'user_id'=>$userId,
'status'=>['in',['success']]
])->order('createtime','desc')->field(['sum(real_pay_price) as pay_price','count(*) as pay_count','createtime as last_pay_time'])->find();
if(!$data){
$data = [
'pay_price'=>0,
'pay_count'=>0,
'last_pay_time'=>0
];
}
$data = $data->toArray();
foreach ($data as &$item){
if(!$item){
$item = 0;
}
}
return $data;
}
}
+480
View File
@@ -0,0 +1,480 @@
<?php
namespace app\admin\model\order;
use think\Model;
use app\admin\model\order\RefundLog;
class Refund extends Model
{
// 表名
protected $name = 'order_evaluate';
// 自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = false;
protected $deleteTime = false;
// 追加属性
protected $append = [
];
// 开始退款
public static function startRefund($order, $item, $refund_money, $remark = '',$saveEquity=1) {
if (is_object($order)) {
$order = $order->toArray();
}
if (is_object($item)) {
$item = $item->toArray();
}
\app\admin\model\order\Item::where([
'id'=>$item['id']
])->update([
'refund_status'=>\app\common\constant\order\Status::REFUND_STATUS_OK,
'refund_price'=>$refund_money + $item['refund_price'],
'refund_time'=>time(),
'refund_msg'=>$remark
]);
\app\common\model\order\Log::set($order['order_no'],"{$item['item_name']}」发起退款,退款金额¥" . $refund_money);
self::refund($order, $item, $refund_money, $remark,$saveEquity);
// 订单退款后
// $data = ['order' => $order, 'item' => $item];
// \think\Hook::listen('order_refund_after', $data);
}
/**
* 退款
* @param $order 所属订单
* @param $item 订单包含商品
* @param $refund_money 退款金额
* @param $remark 备注
* @param $saveEquity 退款后是否保留权益
* @return bool|void
*/
public static function refund($order, $item, $refund_money, $remark = '',$saveEquity=1)
{
if (is_object($order)) {
$order = $order->toArray();
}
if (is_object($item)) {
$item = $item->toArray();
}
// 生成退款单
$refundLog = new RefundLog();
$refundLog->order_no = $order['order_no'];
$refundLog->refund_no = RefundLog::getNo($order['user_id']);
$refundLog->order_item_id = $item['id'];
$refundLog->pay_fee = $order['real_pay_price'];
$refundLog->refund_fee = $refund_money;
$refundLog->pay_type = $order['pay_type'];
$refundLog->save_equity = $saveEquity;
$notify_url = MODULE_URL .'/api/pay/notifyr/pay_type/' . $order['pay_type'] . '/platform/' . $order['source'] . "/uniacid/".UNIACID;
if($refund_money <= 0){
$refundLog->save([
'uniacid'=>UNIACID
]);
self::refundFinish($order, $item, $refundLog);
return true;
}
if (!empty($order['is_virtual_pay']) || $order['pay_type'] == 'virtual_pay') {
return self::virtualPayRefund($order, $item, $refund_money, $remark, $saveEquity, $refundLog);
}
if ($order['pay_type'] == 'wechat' || $order['pay_type'] == 'alipay') {
// 微信|支付宝退款
// 退款数据
$order_data = [
'out_trade_no' => $order['order_no']
];
if ($order['pay_type'] == 'wechat') {
$total_fee = $order['real_pay_price'] * 100;
$refund_fee = $refund_money * 100;
$order_data = array_merge($order_data, [
'out_refund_no' => $refundLog->refund_no,
'total_fee' => $total_fee,
'refund_fee' => $refund_fee,
'refund_desc' => $remark,
]);
} else {
$order_data = array_merge($order_data, [
'out_request_no' => $refundLog->refund_no,
'refund_amount' => $refund_money,
]);
}
try{
// 接入支付账户池:若订单是通过账户池发起的,使用对应商户号退款
$forceAccountId = null;
if (in_array($order['pay_type'], ['wechat', 'alipay'], true)
&& \app\admin\library\project\App::isInstall('paymentpool')) {
$boundAccount = \app\common\library\app\paymentpool\Service::getAccountByOrder($order['order_no']);
if ($boundAccount && !empty($boundAccount['id'])) {
$forceAccountId = intval($boundAccount['id']);
}
}
$pay = new \app\common\library\pay\PayService($order['pay_type'], $order['source'], $notify_url, $forceAccountId);
$result = $pay->refund($order_data);
}catch (\Exception $e){
throw new \app\common\exception\Exception($e->getMessage());
}
if ($order['pay_type'] == 'wechat') {
if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') {
$refundLog->save([
'uniacid'=>UNIACID
]);
return true;
} else {
throw new \app\common\exception\Exception($result['return_msg']);
}
} else {
if (isset($result['code']) && $result['code'] == '10000') {
$refundLog->save([
'uniacid'=>UNIACID
]);
self::refundFinish($order, $item, $refundLog);
return true;
} else {
$errMsg = isset($result['sub_msg']) ? $result['sub_msg'] : (isset($result['msg']) ? $result['msg'] : '退款失败');
throw new \app\common\exception\Exception($errMsg);
}
}
} else if ($order['pay_type'] == 'balance') {
// 余额退款
\app\common\model\User::money($refund_money, $order['user_id'], '订单退款,'.$order['order_no']);
$refundLog->save([
'uniacid'=>UNIACID
]);
self::refundFinish($order, $item, $refundLog);
return true;
} else if ($order['pay_type'] == 'douyinpay') {
$paymentConfig = \app\common\model\config\System::getConfig('douyinpay');
$douyinOrderData = \douyin\Byte::init($paymentConfig)->orderQuery($order['order_no']);
if($douyinOrderData['err_no'] != 0){
throw new \app\common\exception\Exception("发起退款失败;" . ($douyinOrderData && isset($douyinOrderData['err_msg']) ? $douyinOrderData['err_msg'] : ''));
}
$item_order_id = '';
if(isset($douyinOrderData['data']) && !empty($douyinOrderData['data'])){
foreach ($douyinOrderData['data']['item_order_list'] as $skuItem){
if($skuItem['sku_id'] == $item['item_id']){
$item_order_id = $skuItem['item_order_id'];
}
}
}
if(!$item_order_id){
throw new \app\common\exception\Exception("发起退款失败;未查询到订单信息" );
}
$params = [];
$params['order_id'] = $order['transaction_id'];//交易系统侧订单号,长度 <= 64 byte
$params['out_refund_no'] = $refundLog->refund_no;//开发者侧退款单号,长度 <= 64 byte
$params['order_entry_schema'] = [
'path'=>'pages/order/detail/detail',
'params'=>json_encode(['order_no'=>$params['out_refund_no']])
];
$params['refund_total_amount'] = $refund_money * 100;//退款总金额,单位[分]
// $params['notify_url'] = $notify_url;
$params['refund_reason'] = [[
'code'=>101,
'text'=>$remark ? $remark : '不想要了'
]];//退款原因,可填多个,不超过10个
$params['item_order_detail'] = [
[
'item_order_id'=>$item_order_id,
'refund_amount'=>$refund_money * 100
]
];
$data = \douyin\Byte::init($paymentConfig)->refund($params);
if(isset($data['err_no']) && $data['err_no'] == 0){
$refundLog->refund_no = $data['data']['refund_id'];
$refundLog->save([
'uniacid'=>UNIACID
]);
self::douyinRefundQuery($data['data']['refund_id'],$order, $item, $refundLog);
return true;
}else{
throw new \app\common\exception\Exception("发起退款失败;" . ($data && isset($data['err_msg']) ? $data['err_msg'] : ''));
}
} else if ($order->pay_type == 'score') {
// 积分退款,暂不支持积分退款
}
}
/**
* 虚拟支付退款处理
* 使用微信虚拟支付退款接口(refund_order)发起退款任务
* @param array $order 订单信息
* @param array $item 订单商品信息
* @param float $refund_money 退款金额(元)
* @param string $remark 退款原因
* @param int $saveEquity 是否保留权益(1-保留 2-关闭)
* @param RefundLog $refundLog 退款日志对象
* @return bool
* @throws \Exception
*/
private static function virtualPayRefund($order, $item, $refund_money, $remark, $saveEquity, $refundLog)
{
try {
if (is_object($order)) {
$order = $order->toArray();
}
if (!is_array($order)) {
throw new \app\common\exception\Exception('订单数据格式异常');
}
if (is_object($item)) {
$item = $item->toArray();
}
$virtualPayService = new \app\common\library\pay\VirtualPayService();
$orderNo = isset($order['order_no']) ? $order['order_no'] : '';
$userId = isset($order['user_id']) ? $order['user_id'] : 0;
$openid = \app\common\model\order\VirtualPay::getOpenidByOrderNo($orderNo);
\think\Log::info('[VirtualPayRefund] 开始退款 order_no=' . $orderNo . ', user_id=' . $userId . ', openid=' . ($openid ?: '空') . ', refund_money=' . $refund_money);
if (empty($openid)) {
\think\Log::warning('[VirtualPayRefund] 关联表无openid,尝试从oauth表获取 order_no=' . $orderNo . ', user_id=' . $userId);
$oauth = \app\common\model\user\Oauth::where([
'user_id' => $userId,
'provider' => 'Wechat',
'platform' => 'wxMiniProgram'
])->find();
if (!$oauth || empty($oauth->openid)) {
\think\Log::error('[VirtualPayRefund] 无法获取用户openid order_no=' . $orderNo . ', user_id=' . $userId);
throw new \app\common\exception\Exception('用户微信授权信息不存在,无法进行虚拟支付退款');
}
$openid = $oauth->openid;
\think\Log::info('[VirtualPayRefund] 从oauth表获取到openid order_no=' . $orderNo);
}
$realPayPrice = isset($order['real_pay_price']) ? $order['real_pay_price'] : 0;
$orderResult = $virtualPayService->queryOrder($openid, $orderNo);
if (isset($orderResult['errcode']) && $orderResult['errcode'] !== 0) {
throw new \app\common\exception\Exception('查询虚拟支付订单失败:' . ($orderResult['errmsg'] ?? '未知错误'));
}
$leftFee = isset($orderResult['order']['left_fee']) ? intval($orderResult['order']['left_fee']) : intval($realPayPrice * 100);
$refundFee = intval($refund_money * 100);
if ($refundFee <= 0 || $refundFee > $leftFee) {
throw new \app\common\exception\Exception('退款金额不合法,可退金额:' . ($leftFee / 100) . '元');
}
$reasonMap = [
'产品问题' => '1',
'售后问题' => '2',
'用户主动退款' => '3',
'价格问题' => '4',
'其他' => '5',
];
$reasonCode = '0';
foreach ($reasonMap as $key => $code) {
if (strpos($remark, $key) !== false) {
$reasonCode = $code;
break;
}
}
$refundId = $refundLog->refund_no ?: ('VPR' . date('YmdHis') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT));
$result = $virtualPayService->refundOrder(
$openid,
$orderNo,
$refundId,
$leftFee,
$refundFee,
$reasonCode,
'2'
);
if (isset($result['errcode']) && $result['errcode'] !== 0) {
throw new \app\common\exception\Exception('虚拟支付退款失败:' . ($result['errmsg'] ?? '未知错误'));
}
$refundOrderId = $result['refund_order_id'] ?? $refundId;
$refundLog->refund_no = $refundOrderId;
$refundLog->save([
'uniacid' => UNIACID
]);
$pollResult = $virtualPayService->pollRefundStatus($openid, $refundOrderId);
\think\Log::info('[VirtualPayRefund] 退款轮询结果 order_no=' . $orderNo . ', refund_order_id=' . $refundOrderId . ', poll_result=' . json_encode($pollResult));
if (!$pollResult['success']) {
$itemName = isset($item['item_name']) ? $item['item_name'] : '商品';
\app\common\model\order\Log::set($orderNo, "{$itemName}」虚拟支付退款已发起,退款金额¥{$refund_money},退款单号:{$refundOrderId},状态:{$pollResult['message']}");
throw new \app\common\exception\Exception('虚拟支付退款已发起但未完成:' . $pollResult['message']);
}
self::refundFinish($order, $item, $refundLog);
return true;
} catch (\app\common\exception\Exception $e) {
$refundLog->status = -1;
$refundLog->save();
throw $e;
} catch (\Exception $e) {
$refundLog->status = -1;
$refundLog->save();
throw new \app\common\exception\Exception('虚拟支付退款失败,请联系客服');
} catch (\Throwable $e) {
$refundLog->status = -1;
$refundLog->save();
throw new \app\common\exception\Exception('虚拟支付退款失败,请联系客服');
}
}
/**
* 退款完成
* @param $order
* @param $item
* @param $refundLog
* @return void
*/
public static function refundFinish($order, $item, $refundLog)
{
// 退款完成
$refundLog->status = 1;
$refundLog->save();
//检查是否已全额退款,满足条件变更订单状态为已退款
$refundPrice = \app\admin\model\order\Item::where([
'order_no'=>$order['order_no']
])->sum('refund_price');
$orderItem = \app\admin\model\order\Item::get($item['id']);
if($refundPrice >= $order['real_pay_price']){
\app\admin\model\order\Order::where([
'order_no'=>$order['order_no']
])->update([
'status'=>\app\common\constant\order\Status::STATUS_REFUND
]);
}
if($refundLog->save_equity == 2 && $orderItem->refund_can_use != 2){
//关闭权益
$orderItem->refund_can_use = 2;
if ($item['goods_type'] == 'composite') {
// 组合商品:关闭所有子商品权限
if (\app\admin\library\project\App::isInstall('composite')) {
$snapshoot = $item['snapshoot'];
if (is_string($snapshoot)) {
$snapshoot = json_decode($snapshoot, true);
}
if (!empty($snapshoot['sub_goods_list'])) {
foreach ($snapshoot['sub_goods_list'] as $subGoods) {
\app\common\model\user\Subscription::cancelUserSubscription(
$item['user_id'],
$subGoods['sub_goods_id'],
$subGoods['sub_goods_type'],
''
);
}
}
}
} else {
\app\common\model\user\Subscription::cancelUserSubscription($item['user_id'],$item['item_id'],$item['goods_type'],$item['extend_data']);
}
}
// 全额退款时,组合商品返还销量
if ($item['goods_type'] == 'composite' && $refundPrice >= $order['real_pay_price']) {
if (\app\admin\library\project\App::isInstall('composite')) {
\app\common\behavior\order\Payed::compositeRefundHandle($item, $order, true);
}
}
$orderItem->refund_status = \app\common\constant\order\Status::REFUND_STATUS_FINISH;
$orderItem->save();
\app\common\model\order\Log::set($order['order_no'],"{$item['item_name']}」退款成功");
}
/**
*
* @param $refund_id
* @param $order
* @param $item
* @param $refundLog
* @param $num
* @return void
*/
public static function douyinRefundQuery($refund_id,$order, $item, $refundLog,$num=0)
{
$paymentConfig = \app\common\model\config\System::getConfig('douyinpay');
$data = \douyin\Byte::init($paymentConfig)->refundQuery($refund_id);
if(isset($data['err_no']) && $data['err_no'] == 0 && !empty($data['data']['refund_list'])){
switch ($data['data']['refund_list'][0]['refund_status']){
case 'SUCCESS':
self::refundFinish($order, $item, $refundLog);
break;
case 'PROCESSING':
sleep(2);
if($num < 3){
self::douyinRefundQuery($refund_id,$order, $item, $refundLog,($num+1));
}else{
self::refundFinish($order, $item, $refundLog);
}
break;
case 'FAIL':
$refundLog->status = -1;
$refundLog->save();
throw new \app\common\exception\Exception($data['data']['refund_list'][0]['message']);
break;
}
}else{
$refundLog->status = -1;
$refundLog->save();
}
}
}
@@ -0,0 +1,71 @@
<?php
namespace app\admin\model\order;
use think\Model;
class RefundLog extends Model
{
// 表名
protected $name = 'order_refund_log';
// 自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = 'updatetime';
protected $deleteTime = false;
// 追加属性
protected $append = [
'status_text'
];
// 订单状态
const STATUS_INVALID = -2;
const STATUS_CANCEL = -1;
const STATUS_NOPAY = 0;
const STATUS_PAYED = 1;
const STATUS_FINISH = 2;
public function getStatusList()
{
return ['0' => __('Status 0'), '1' => __('Status 1'), '-1' => __('Status -1')];
}
public function getStatusTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['status']) ? $data['status'] : '');
$list = $this->getStatusList();
return isset($list[$value]) ? $list[$value] : '';
}
public function order()
{
return $this->belongsTo('app\admin\model\Order', 'order_no', 'order_no', [], 'LEFT')->setEagerlyType(0);
}
// 获取退款单号
public static function getNo($user_id)
{
$rand = $user_id < 9999 ? mt_rand(100000, 99999999) : mt_rand(100, 99999);
$order_sn = date('Yhis') . $rand;
$id = str_pad($user_id, (24 - strlen($order_sn)), '0', STR_PAD_BOTH);
return 'R' . $order_sn . $id;
}
public function item()
{
return $this->belongsTo('app\admin\model\order\Item', 'order_item_id', 'id', [], 'LEFT')->setEagerlyType(0);
}
}