feat(fsgx): 完成全部24项开发任务 Phase1-7

Phase1 后端核心:
- 新增 fsgx_v1.sql 迁移脚本(is_queue_goods/frozen_points/available_points/no_assess)
- SystemConfigServices 返佣设置扩展(周期人数/分档比例/范围/时机)
- StoreOrderCreateServices 周期循环佣金计算
- StoreOrderTakeServices 佣金发放后同步冻结积分
- StoreProductServices/StoreProduct 保存 is_queue_goods

Phase2 后端接口:
- GET /api/hjf/brokerage/progress 佣金周期进度
- GET /api/hjf/assets/overview 资产总览
- HjfPointsServices 每日 frozen_points 0.4‰ 释放定时任务
- PUT /adminapi/hjf/member/{uid}/no_assess 不考核接口
- GET /adminapi/hjf/points/release_log 积分日志接口

Phase3 前端清理:
- hjfCustom.js 路由精简(仅保留 points/log)
- hjfQueue.js/hjfMember.js API 清理/重定向至 CRMEB 原生接口
- pages.json 公排→推荐佣金/佣金记录/佣金规则

Phase4-5 前端改造:
- queue/status.vue 推荐佣金进度页整体重写
- 商品详情/订单确认/支付结果页文案与逻辑改造
- 个人中心/资产页/引导页/规则页文案改造
- HjfQueueProgress/HjfRefundNotice/HjfAssetCard 组件改造
- 推广中心嵌入佣金进度摘要
- hjfMockData.js 全量更新(公排字段→佣金字段)

Phase6 Admin 增强:
- 用户列表新增 frozen_points/available_points 列及不考核操作按钮
- hjfPoints.js USE_MOCK=false 对接真实积分日志接口

Phase7 配置文档:
- docs/fsgx-phase7-config-checklist.md 后台配置与全链路验收清单

Made-with: Cursor
This commit is contained in:
apple
2026-03-23 22:32:19 +08:00
parent 788ee0c0c0
commit 434aa8c69d
13098 changed files with 2008990 additions and 961 deletions

View File

@@ -0,0 +1,287 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\controller\api\v1\order;
use app\services\order\OtherOrderServices;
use app\services\pay\OrderPayServices;
use app\services\pay\PayServices;
use app\services\pay\YuePayServices;
use app\services\user\member\MemberCardServices;
use app\services\user\UserServices;
use crmeb\services\CacheService;
use crmeb\services\SystemConfigService;
use app\Request;
use think\annotation\Inject;
/**
* Class OtherOrder
* @package app\api\controller\v1\order
*/
class OtherOrder
{
/**
* @var OtherOrderServices
*/
#[Inject]
protected OtherOrderServices $services;
/**
* 计算会员线下付款金额
* @param Request $request
* @return mixed
*/
public function computed_offline_pay_price(Request $request)
{
[$pay_price] = $request->getMore([
['pay_price', 0]
], true);
if (!$pay_price || !is_numeric($pay_price)) return app('json')->fail('请输入付款金额');
$uid = $request->uid();
/** @var UserServices $userService */
$userService = app()->make(UserServices::class);
//会员线下享受折扣
if ($userService->checkUserIsSvip($uid)) {
//看是否开启线下享受折扣
/** @var MemberCardServices $memberCardService */
$memberCardService = app()->make(MemberCardServices::class);
$offline_rule_number = $memberCardService->isOpenMemberCardCache('offline');
if ($offline_rule_number) {
$pay_price = bcmul($pay_price, bcdiv($offline_rule_number, '100', 2), 2);
} else {
$pay_price = 0;
}
} else {
$pay_price = 0;
}
return app('json')->successful(['pay_price' => $pay_price]);
}
/**
* @param Request $request
* @return mixed
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function create(Request $request)
{
$uid = (int)$request->uid();
/** @var OtherOrderServices $OtherOrderServices */
$OtherOrderServices = app()->make(OtherOrderServices::class);
[$payType, $type, $from, $memberType, $price, $money, $quitUrl] = $request->postMore([
['pay_type', 'yue'],
['type', 0],
['from', 'weixin'],
['member_type', ''],
['price', 0.00],
['money', 0.00],
['quitUrl', ''],
], true);
$payType = strtolower($payType);
if (in_array($type, [1, 2])) {
/** @var MemberCardServices $memberCardService */
$memberCardService = app()->make(MemberCardServices::class);
$isOpenMember = $memberCardService->isOpenMemberCardCache();
if (!$isOpenMember) return app('json')->fail('付费会员功能暂未开启!');
}
$channelType = $request->user('user_type');
$order = $OtherOrderServices->createOrder($uid, $channelType, $memberType, $price, $payType, $type, $money);
if ($order === false) return app('json')->fail('支付数据生成失败');
$order = $order->toArray();
$order_id = $order['order_id'];
if($type == 3){
$info = compact('order_id');
switch ($payType) {
case PayServices::WEIXIN_PAY:
if ($order['paid']) return app('json')->fail('已支付!');
//支付金额为0
if (bcsub((string)$order['pay_price'], '0', 2) <= 0) {
//创建订单jspay支付
$payPriceStatus = $OtherOrderServices->zeroYuanPayment($order);
if ($payPriceStatus)//0元支付成功
return app('json')->status('success', '微信支付成功', $info);
else
return app('json')->status('pay_error');
} else {
/** @var OrderPayServices $payServices */
$payServices = app()->make(OrderPayServices::class);
$info['jsConfig'] = $payServices->orderPay($order, $from);
if ($from == 'weixinh5') {
return app('json')->status('wechat_h5_pay', '前往支付', $info);
} else {
return app('json')->status('wechat_pay', '前往支付', $info);
}
}
break;
case PayServices::YUE_PAY:
/** @var YuePayServices $yueServices */
$yueServices = app()->make(YuePayServices::class);
$pay = $yueServices->yueOrderPay($order, $uid);
if ($pay['status'] === true)
return app('json')->status('success', '余额支付成功', $info);
else {
if (is_array($pay))
return app('json')->status($pay['status'], $pay['msg'], $info);
else
return app('json')->status('pay_error', $pay);
}
break;
case PayServices::ALIPAY_PAY:
if (!$quitUrl && ($request->isH5() || $request->isWechat())) {
return app('json')->status('pay_error', '请传入支付宝支付回调URL', $info);
}
//支付金额为0
if (bcsub((string)$order['pay_price'], '0', 2) <= 0) {
//创建订单jspay支付
$payPriceStatus = $OtherOrderServices->zeroYuanPayment($order);
if ($payPriceStatus)//0元支付成功
return app('json')->status('success', '支付宝支付成功', $info);
else
return app('json')->status('pay_error');
} else {
/** @var OrderPayServices $payServices */
$payServices = app()->make(OrderPayServices::class);
$info['jsConfig'] = $payServices->alipayOrder($order, $quitUrl, $request->isRoutine());
$payKey = md5($order['order_id']);
CacheService::set($payKey, ['order_id' => $order['order_id'], 'other_pay_type' => true], 300);
$info['pay_key'] = $payKey;
return app('json')->status(PayServices::ALIPAY_PAY . '_pay', '前往支付', $info);
}
break;
case PayServices::OFFLINE_PAY:
return app('json')->status('success', '前往支付', $info);
break;
default:
return app('json')->fail('未知支付方式');
}
}else{
if ($order_id) {
return app('json')->successful('订单创建成功', ['order_id' => $order_id]);
} else {
return app('json')->fail('订单生成失败!');
}
}
}
/**
* 会员订单支付
* @param Request $request
* @return \think\Response
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @throws \throwable
*/
public function create_pay(Request $request)
{
$uid = (int)$request->uid();
/** @var OtherOrderServices $OtherOrderServices */
$OtherOrderServices = app()->make(OtherOrderServices::class);
[$uni, $payType, $from, $quitUrl] = $request->postMore([
['uni', ''],
['paytype', 'weixin'],
['from', 'weixin'],
['quitUrl', '']
], true);
$payType = strtolower($payType);
if (!$uni) return app('json')->fail('参数错误!');
$order = $OtherOrderServices->getOne(['order_id' => $uni]);
if ($order === false) return app('json')->fail('支付数据获取失败');
if ($order['pay_type'] != $payType) {
$res = $OtherOrderServices->update(['order_id' => $uni], ['pay_type' => $payType]);
if (!$res) return app('json')->fail('支付方式修改失败');
}
$orderInfo = $order->toArray();
$order_id = $orderInfo['order_id'];
$info = compact('order_id');
switch ($payType) {
case PayServices::WEIXIN_PAY:
if ($orderInfo['paid']) return app('json')->fail('已支付!');
//支付金额为0
if (bcsub((string)$orderInfo['pay_price'], '0', 2) <= 0) {
//创建订单jspay支付
$payPriceStatus = $OtherOrderServices->zeroYuanPayment($orderInfo);
if ($payPriceStatus)//0元支付成功
return app('json')->status('success', '微信支付成功', $info);
else
return app('json')->status('pay_error');
} else {
/** @var OrderPayServices $payServices */
$payServices = app()->make(OrderPayServices::class);
$info['jsConfig'] = $payServices->orderPay($orderInfo, $from);
if ($from == 'weixinh5') {
return app('json')->status('wechat_h5_pay', '前往支付', $info);
} else {
return app('json')->status('wechat_pay', '前往支付', $info);
}
}
break;
case PayServices::YUE_PAY:
/** @var YuePayServices $yueServices */
$yueServices = app()->make(YuePayServices::class);
$pay = $yueServices->yueOrderPay($orderInfo, $uid);
if ($pay['status'] === true)
return app('json')->status('success', '余额支付成功', $info);
else {
if (is_array($pay))
return app('json')->status($pay['status'], $pay['msg'], $info);
else
return app('json')->status('pay_error', $pay);
}
break;
case PayServices::ALIPAY_PAY:
if (!$quitUrl && ($request->isH5() || $request->isWechat())) {
return app('json')->status('pay_error', '请传入支付宝支付回调URL', $info);
}
//支付金额为0
if (bcsub((string)$orderInfo['pay_price'], '0', 2) <= 0) {
//创建订单jspay支付
$payPriceStatus = $OtherOrderServices->zeroYuanPayment($orderInfo);
if ($payPriceStatus)//0元支付成功
return app('json')->status('success', '支付宝支付成功', $info);
else
return app('json')->status('pay_error');
} else {
/** @var OrderPayServices $payServices */
$payServices = app()->make(OrderPayServices::class);
$info['jsConfig'] = $payServices->alipayOrder($orderInfo, $quitUrl, $request->isRoutine());
$payKey = md5($orderInfo['order_id']);
CacheService::set($payKey, ['order_id' => $orderInfo['order_id'], 'other_pay_type' => true], 300);
$info['pay_key'] = $payKey;
return app('json')->status(PayServices::ALIPAY_PAY . '_pay', '前往支付', $info);
}
break;
case PayServices::OFFLINE_PAY:
return app('json')->status('success', '前往支付', $info);
break;
default:
return app('json')->fail('未知支付方式');
}
}
/**
* 线下支付方式
* @return mixed
*/
public function pay_type(Request $request)
{
$payType = SystemConfigService::more(['ali_pay_status', 'pay_weixin_open', 'site_name', 'balance_func_status']);
$payType['now_money'] = $request->user('now_money');
$payType['offline_pay_status'] = true;
$payType['yue_pay_status'] = (int)$payType['balance_func_status'] ? 1 : 0;//余额支付 1 开启 2 关闭
unset($payType['balance_func_status']);
return app('json')->successful($payType);
}
}

View File

@@ -0,0 +1,258 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\controller\api\v1\order;
use app\Request;
use app\services\order\StoreCartServices;
use app\services\activity\discounts\StoreDiscountsServices;
use crmeb\services\CacheService;
use think\annotation\Inject;
/**
* 购物车类
* Class StoreCart
* @package app\api\controller\store
*/
class StoreCart
{
/**
* @var StoreCartServices
*/
#[Inject]
protected StoreCartServices $services;
/**
* 购物车 列表
* @param Request $request
* @return mixed
*/
public function lst(Request $request)
{
[$status, $latitude, $longitude, $store_id, $is_channel, $is_send_gift] = $request->postMore([
['status', 1],//购物车商品状态
['latitude', ''],
['longitude', ''],
['store_id', 0],
['is_channel', 0],
['is_send_gift', 0],
], true);
if (!checkCoordinates($longitude, $latitude)) {
return app('json')->fail('参数错误');
}
$this->services->setItem('latitude', $latitude)->setItem('longitude', $longitude)->setItem('store_id', (int)$store_id)->setItem('status', $status);
$result = $this->services->getUserCartList((int)$request->uid(), (int)$status, [], -1, 0, false, $is_channel, (int)$is_send_gift);
$this->services->reset();
$result['valid'] = $this->services->getReturnCartList($result['valid'], $result['promotions']);
unset($result['promotions']);
return app('json')->successful($result);
}
/**
* 购物车 添加
* @param Request $request
* @return mixed
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function add(Request $request)
{
$where = $request->postMore([
['productId', 0],//普通商品编号
[['cartNum', 'd'], 1], //购物车数量
['uniqueId', ''],//属性唯一值
[['new', 'd'], 0],// 1 加入购物车直接购买 0 加入购物车
[['is_new', 'd'], 0],// 1 加入购物车直接购买 0 加入购物车
[['secKillId', 'd'], 0],//秒杀商品ID
[['bargainId', 'd'], 0],//砍价商品ID
[['combinationId', 'd'], 0],//拼团商品ID
[['storeIntegralId', 'd'], 0],//积分商品ID
[['discountId', 'd'], 0],//优惠套餐ID
['discountInfos', []],//优惠套餐商品ID
[['newcomerId', 'd'], 0],//新人专享商品ID
[['luckRecordId', 'd'], 0],//抽奖记录ID
[['key', 's'], ''],//直接购买购物车ID 再次累加1
[['is_set', 'd'], 0],//1直接设置购物车数量 0累加
[['is_channel', 'd'], 0],//1采购商 0普通
[['is_send_gift', 'd'], 0],//1送礼 0普通
]);
if ($where['is_new'] || $where['new']) $new = true;
else $new = false;
if (!$where['productId'] && !$where['discountId']) {
return app('json')->fail('参数错误');
}
$type = 0;
$uid = (int)$request->uid();
$activityId = 0;
$this->services->setItem('key', $where['key'] ?? '');
$this->services->setItem('is_set', $where['is_set'] ?? 0);
if ($where['discountId']) {//套餐商品
$type = 5;
/** @var StoreDiscountsServices $discountService */
$discountService = app()->make(StoreDiscountsServices::class);
$discounts = $discountService->get((int)$where['discountId'], ['id', 'is_limit', 'limit_num']);
if (!$discounts) {
return app('json')->fail('套餐商品未找到!');
}
//套餐限量
if ($discounts['is_limit']) {
if ($discounts['limit_num'] <= 0) {
return app('json')->fail('套餐限量不足');
}
if (!CacheService::checkStock(md5($discounts['id']), 1, $type)) {
return app('json')->fail('套餐限量不足');
}
}
$cartIds = [];
$cartNum = 0;
$activityId = (int)$where['discountId'];
foreach ($where['discountInfos'] as $info) {
[$cartId, $cartNum] = $this->services->setCart($uid, (int)$info['product_id'], 1, $info['unique'], $type, $new, $activityId, (int)$info['id']);
$cartIds[] = $cartId;
}
} else {
if ($where['secKillId']) {
$type = 1;
$activityId = $where['secKillId'];
} elseif ($where['bargainId']) {
$type = 2;
$activityId = $where['bargainId'];
} elseif ($where['combinationId']) {
$type = 3;
$activityId = $where['combinationId'];
} elseif ($where['storeIntegralId']) {
$type = 4;
$activityId = $where['storeIntegralId'];
} elseif ($where['newcomerId']) {
$type = 7;
$activityId = $where['newcomerId'];
} elseif ($where['luckRecordId']) {
$type = 8;
$activityId = $where['luckRecordId'];
} elseif ($where['is_send_gift'] == 1) {
$type = 10;
}
[$cartIds, $cartNum] = $this->services->setCart($uid, (int)$where['productId'], (int)$where['cartNum'], $where['uniqueId'], $type, $new, (int)$activityId, 0, $where['is_channel'], $where['is_send_gift']);
}
$this->services->reset();
if (!$cartIds) {
return app('json')->fail('添加失败');
} else {
//更新秒杀详情缓存
$this->services->cacheTag('Cart_Nums_' . $uid)->clear();
return app('json')->successful('ok', ['cartId' => $cartIds, 'cartNum' => $cartNum]);
}
}
/**
* 购物车 删除商品
* @param Request $request
* @return mixed
*/
public function del(Request $request)
{
$where = $request->postMore([
['ids', ''],//购物车编号
]);
$where['ids'] = is_array($where['ids']) ? $where['ids'] : stringToIntArray($where['ids']);
if (!count($where['ids']))
return app('json')->fail('参数错误!');
if ($this->services->removeUserCart((int)$request->uid(), $where['ids'])) {
$this->services->cacheTag('Cart_Nums_' . $request->uid())->clear();
return app('json')->successful();
}
return app('json')->fail('清除失败!');
}
/**
* 购物车 修改商品数量
* @param Request $request
* @return mixed
* @throws \think\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function num(Request $request)
{
$where = $request->postMore([
['id', 0],//购物车编号
['type', 1],//1购物车 id,2商品 id
['number', 0],//购物车编号
]);
if ($where['type'] == 2) {
$where['id'] = $this->services->value(['product_id' => $where['id'], 'is_pay' => 0, 'uid' => $request->uid()], 'id');
}
unset($where['type']);
if (!$where['id'] || !$where['number'] || !is_numeric($where['id']) || !is_numeric($where['number'])) return app('json')->fail('参数错误!');
$res = $this->services->changeUserCartNum($where['id'], $where['number'], $request->uid());
if ($res) {
$this->services->cacheTag('Cart_Nums_' . $request->uid())->clear();
return app('json')->successful();
} else {
return app('json')->fail('修改失败');
}
}
/**
* 购物车 统计 数量 价格
* @param Request $request
* @return mixed
*/
public function count(Request $request)
{
[$numType, $store_id, $is_channel] = $request->postMore([
['numType', true],//购物车编号
['store_id', 0],
['is_channel', 0],
], true);
$uid = (int)$request->uid();
return app('json')->success('ok', $this->services->getUserCartCount($uid, $numType, (int)$store_id, (int)$is_channel));
}
/**
* 购物车重选
* @param Request $request
* @return mixed
*/
public function reChange(Request $request)
{
[$cart_id, $product_id, $unique] = $request->postMore([
['cart_id', 0],
['product_id', 0],
['unique', '']
], true);
$this->services->modifyCart($cart_id, $product_id, $unique);
$this->services->cacheTag('Cart_Nums_' . $request->uid())->clear();
return app('json')->success('重选成功');
}
/**
* 计算用户购物车商品(优惠活动、最优优惠券)
* @param Request $request
* @return mixed
*/
public function computeCart(Request $request)
{
[$cartId, $shipping_type, $is_send_gift] = $request->postMore([
'cartId',
['shipping_type', -1],
['is_send_gift', 0],
], true);
$uid = (int)$request->uid();
$result = $this->services->computeUserCart($uid, $cartId, (int)$shipping_type, false, (int)$is_send_gift);
return app('json')->success($result);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\controller\api\v1\order;
use app\Request;
use app\services\order\StoreOrderRefundServices;
use think\annotation\Inject;
/**
* 售后订单
* Class StoreOrderRefundController
* @package app\controller\api\v1\order
*/
class StoreOrderRefund
{
/**
* @var StoreOrderRefundServices
*/
#[Inject]
protected StoreOrderRefundServices $services;
/**
* 订单列表
* @param Request $request
* @return mixed
*/
public function lst(Request $request)
{
$where = $request->getMore([
[['search', 's'], '', '', 'real_name'],
['refund_type', '', '', 'refundTypes'],
['channel', 0],
]);
$where['uid'] = $request->uid();
$list = $this->services->getRefundOrderList($where);
return app('json')->successful($list);
}
/**
* 订单详情
* @param Request $request
* @param $uni
* @return mixed
*/
public function detail(StoreOrderRefundServices $services, Request $request, $uni)
{
$orderData = $services->refundDetail($uni);
return app('json')->successful('ok', $orderData);
}
/**
* 取消申请
* @param $id
* @return mixed
*/
public function cancelApply(Request $request, $uni)
{
if (!strlen(trim($uni))) return app('json')->fail('参数错误');
$uid = (int)$request->uid();
$this->services->cancelApplyRefund($uid, $uni);
return app('json')->success('取消成功');
}
/**
* 删除已退款和拒绝退款的订单
* @param Request $request
* @param $uni
* @return mixed
*/
public function delRefundOrder(Request $request, $uni)
{
if (!strlen(trim($uni))) return app('json')->fail('参数错误');
$uid = (int)$request->uid();
$this->services->delRefundOrder($uid, $uni);
return app('json')->success('删除成功');
}
/**
* 再次申请
* @param $id
* @return mixed
*/
public function againRefundOrder(Request $request, $id)
{
if (!strlen(trim($id))) return app('json')->fail('参数错误');
$uid = (int)$request->uid();
$this->services->againRefundOrder($uid, (int)$id);
return app('json')->success('申请成功');
}
}