diff --git a/backend/application/api/controller/Demand.php b/backend/application/api/controller/Demand.php index 8e70585..7b20f0b 100644 --- a/backend/application/api/controller/Demand.php +++ b/backend/application/api/controller/Demand.php @@ -120,6 +120,9 @@ class Demand extends Api $button = ['name' => '查看详情', 'theme' => 'light']; // key 为按钮动作标识(复用 DemandService::BUTTON_* 常量), // 前端据此判断按钮类型,避免依赖中文文案;无动作的展示型按钮 key 为空 + if ((int)$item->status === DemandService::STATUS_FINISHED) { + return ['key' => DemandService::BUTTON_FINISHED, 'name' => '交易完成', 'theme' => 'light']; + } if ($opType == 'all') { if ($item->status == 1 && $item->isMyWin && $item->winBidding->bidding_status == 2 && $item->winBidding->process_status == 1) { $button = ['key' => DemandService::BUTTON_CONFIRM, 'name' => '确认接单', 'theme' => 'danger']; @@ -196,9 +199,12 @@ class Demand extends Api $bidUserIds = array_column($demand_bidding, 'user_id'); $item->demand_biddings_count = count($demand_bidding); $item->is_bid = in_array($user_id, $bidUserIds); + $item->isMy = $item->user_id == $user_id; $item->isMyWin = $item->winBidding && $item->winBidding->user_id == $user_id; $item->hasDelivery = in_array($item->id, $deliveredDemandIds); $item->button = $btnNameFun($opType, $item); + // 列表不返回报价、合同和中标明细,避免其它开发者看到成交价与交付相关信息 + $item->hidden(['demandBiddings', 'demandContracts', 'winBidding']); }); $this->success('', $list); } catch (\Exception $e) { @@ -222,6 +228,13 @@ class Demand extends Api if (!$info) { throw new \Exception('该需求不存在'); } + $info->isMy = $user_id == $info->user_id;//我发布 + $info->isMyWin = $info->winBidding && $info->winBidding->user_id == $user_id;//我中标 + $canSeeDeal = $info->isMy || $info->isMyWin; + // 交易完成的订单,只有发布者和接单开发者可以查看 + if ((int)$info->status === DemandService::STATUS_FINISHED && !$canSeeDeal) { + throw new \Exception('仅发布者和接单开发者可查看'); + } //更新浏览次数 DemandService::where($where)->setInc('views'); $steps = [ @@ -239,12 +252,10 @@ class Demand extends Api 'steps' => $steps, 'current' => max($ins - 1, 0) ]; - //我的报价 - $info->myBidding = DemandBiddingService::getOne(['demand_id' => $id, 'user_id' => $user_id]); - //中标报价 -// $info->winBidding = DemandBiddingService::getOne(['demand_id' => $id, 'bidding_status' => DemandBiddingService::STATUS_BIDDING_WIN]); - $info->isMy = $user_id == $info->user_id;//我发布 - $info->isMyWin = $info->winBidding && $info->winBidding->user_id == $user_id;//我中标 + // 招标中返回自己的报价,便于继续投标;成交报价只给发布者和接单开发者 + $info->myBidding = ((int)$info->status === DemandService::STATUS_BIDDING || $canSeeDeal) + ? DemandBiddingService::getOne(['demand_id' => $id, 'user_id' => $user_id]) + : null; //小程序内无法直接识别二维码,需先长按保存再用微信扫一扫 $qrcodeText = '请使用微信扫码进群沟通'; if ($user_id == $info->user_id) { @@ -257,8 +268,13 @@ class Demand extends Api 'url' => $fl, 'name' => urldecode(basename($fl)) ], $info->files); - //交付内容(订单发货表,开发者发货后展示) - $info->delivery = OrderShipService::getDemandDelivery($id); + // 成交报价、合同和交付内容仅发布者与接单开发者可见 + if ($canSeeDeal) { + $info->delivery = OrderShipService::getDemandDelivery($id); + } else { + $info->delivery = null; + $info->hidden(['winBidding', 'demandContracts']); + } $this->success('', $info); } catch (\Exception $e) { @@ -273,8 +289,17 @@ class Demand extends Api { try { $demandId = $this->request->param('demandId', 0); + $userId = $this->auth->id ?? 0; + $demand = DemandService::getOne(['id' => $demandId], ['winBidding']); + if (!$demand) { + $this->error('该需求不存在'); + } + $isMy = $demand->user_id == $userId; + $isMyWin = $demand->winBidding && $demand->winBidding->user_id == $userId; + if (!$isMy && !$isMyWin) { + $this->error('仅发布者和接单开发者可查看'); + } $info = ContractService::getWinBiddingContract($demandId); - $demand = DemandService::getOne(['id' => $demandId]); $platform = $demand ? VirtualPayService::normalizePayPlatform($demand->pay_platform) : ''; $info->pay_platform = $platform !== '' ? $platform : 'android'; if (empty($info->data_json['customData']) && ($firstContract = ContractService::getFirstContract($demandId))) { diff --git a/backend/application/common/command/DemandDeal.php b/backend/application/common/command/DemandDeal.php index e39e004..2189220 100644 --- a/backend/application/common/command/DemandDeal.php +++ b/backend/application/common/command/DemandDeal.php @@ -96,7 +96,7 @@ class DemandDeal extends Command Db::transaction(function () use ($item) { //根据订单列表查找冻结金额日志,根据冻结日志解冻冻结金额到余额 $userId = $item->winBidding->user_id; - $settleTotalAmount = 0; + $settleTotalAmount = '0'; foreach ($item->orders as $order) { $logWhere = [ 'user_id' => $userId, @@ -109,7 +109,7 @@ class DemandDeal extends Command $log = AccountLogService::where($logWhere)->find(); if ($log) { $amount = $log->amount; - $settleTotalAmount += $amount; + $settleTotalAmount = bcadd($settleTotalAmount, (string)$amount, 2); //写入余额日志 $title = OrderService::ORDER_TYPE_MAP[$order->order_type] ?? '需求收益'; $log_res = AccountLogService::addLog(AccountLogService::ACCOUNT_TYPE_FREEZE, $userId, $amount, $title . '解冻-冻结金额扣除', AccountLogService::TYPE_UNFREEZE, 0, AccountLogService::INC_EXP_EXPEND, $order->id, $order->total_amount); @@ -134,7 +134,7 @@ class DemandDeal extends Command $msgData = [ 'values' => [ $item->parent_order_no, - round($settleTotalAmount, 2), + $settleTotalAmount, SubscribeMessageService::MSG_MAP[SubscribeMessageService::MSG_TEMP_DEMAND_SETTLE]['remark'], ] ]; diff --git a/backend/application/common/service/DemandService.php b/backend/application/common/service/DemandService.php index 47cef90..9f5024e 100644 --- a/backend/application/common/service/DemandService.php +++ b/backend/application/common/service/DemandService.php @@ -71,6 +71,7 @@ class DemandService const BUTTON_ACCEPT_PASSED = 'acceptPassed'; //验收通过 const BUTTON_CONFIRM_RECEIPT = 'confirmReceipt'; //确认收货 const BUTTON_DELIVER_GOODS = 'deliverGoods'; //开发者发货 + const BUTTON_FINISHED = 'finished'; //交易完成,仅发布者和接单开发者可进入详情 /** * 获取平台服务费比例(0~1),后台填写大于 1 时按百分比处理,如 10 表示 10% diff --git a/backend/application/common/service/MessageService.php b/backend/application/common/service/MessageService.php index bbc48b6..3d9249c 100644 --- a/backend/application/common/service/MessageService.php +++ b/backend/application/common/service/MessageService.php @@ -69,11 +69,17 @@ class MessageService //按模板字段名组装可读内容:工单号:xxx;服务项目:xxx;...;备注:xxx $values = $msgData['values'] ?? []; $valueNames = $template['valueNames'] ?? []; + $fields = $template['fields'] ?? []; $lines = []; foreach ($valueNames as $key => $name) { - if (isset($values[$key]) && $values[$key] !== '') { - $lines[] = $name . ':' . $values[$key]; + if (!isset($values[$key]) || $values[$key] === '') { + continue; } + $value = $values[$key]; + if (strpos($fields[$key] ?? '', 'amount') === 0) { + $value = SubscribeMessageService::formatAmount($value); + } + $lines[] = $name . ':' . $value; } // 备注处理:避免正文出现两行「备注」或语义冲突 // 1) 模板字段里已含「备注」时,发送方自定义了备注文案,不再追加模板默认 remark diff --git a/backend/application/common/service/SubscribeMessageService.php b/backend/application/common/service/SubscribeMessageService.php index 5794313..def129c 100644 --- a/backend/application/common/service/SubscribeMessageService.php +++ b/backend/application/common/service/SubscribeMessageService.php @@ -58,7 +58,7 @@ class SubscribeMessageService 'remark' => '该工单已结算完成', /* 订单编号:200912162851392635fdbf4149 - 结算金额:99.9元 + 结算金额:¥99.9 结算说明:订单已收货结算完成 */ 'fields' => ['character_string4', 'amount1', 'thing2'], @@ -231,11 +231,46 @@ class SubscribeMessageService public static function packData($template_id, $values): array { $fields = self::MSG_MAP[$template_id]['fields'] ?? []; + $param_data = []; foreach ($fields as $key => $field) { - $param_data[$field] = ['value' => $values[$key] ?? '']; + $value = $values[$key] ?? ''; + //微信 amount 类型不带币种符号时会固定展示 5 位小数,如 2.37 显示成 2.37000 + if (strpos($field, 'amount') === 0) { + $value = self::formatAmount($value); + } + $param_data[$field] = ['value' => $value]; } - return $param_data ?? []; + return $param_data; + } + + /** + * 订阅消息金额:最多两位小数,去掉末尾多余的 0,并加上币种符号。 + * 微信 amount 类型不带币种符号时会把 2.37 展示成 2.37000。 + * @param mixed $amount + * @return string 如 ¥2.37、¥2.3、¥2 + */ + public static function formatAmount($amount): string + { + if ($amount === '' || $amount === null || !is_numeric($amount)) { + return (string)$amount; + } + $number = (string)$amount; + if (stripos($number, 'e') !== false) { + $number = sprintf('%.8F', (float)$amount); + } + $negative = strncmp($number, '-', 1) === 0; + if ($negative) { + $number = substr($number, 1); + } + //先加 0.005 再截到 2 位,等价于四舍五入,避免 bcmath 直接截断 + $rounded = bcadd($number, '0.005', 2); + $rounded = rtrim(rtrim($rounded, '0'), '.'); + if ($rounded === '') { + $rounded = '0'; + } + + return '¥' . ($negative ? '-' : '') . $rounded; } } diff --git a/frontend/miniapp/pages/demand/contract/index.js b/frontend/miniapp/pages/demand/contract/index.js index 104c857..204ece2 100644 --- a/frontend/miniapp/pages/demand/contract/index.js +++ b/frontend/miniapp/pages/demand/contract/index.js @@ -531,41 +531,53 @@ Page({ return !!error; }, - // 表单验证 + // 表单验证:按页面从上到下的顺序,提示第一个未填字段 validateForm() { const { formData } = this.data; + const missing = (title) => { + wx.showToast({ + title, + icon: 'none' + }); + return false; + }; + const blank = (value) => String(value === undefined || value === null ? '' : value).trim() === ''; + const emptyAmount = (value) => blank(value) || Number(value) === 0; const contractType = formData.contractType; if (contractType === 'personal') { // 身份证号暂不采集 - if (!formData.name || !formData.phone) { - wx.showToast({ - title: '请填写完整的个人信息', - icon: 'none' - }); - return false; + if (blank(formData.name)) { + return missing('请填写个人姓名'); + } + if (blank(formData.phone)) { + return missing('请填写联系电话'); } if (this.hasPhoneError(formData.phone, 'phoneError')) { return false; } } else { - if (!formData.companyName || !formData.legalPerson || !formData.companyPhone) { - wx.showToast({ - title: '请填写完整的企业信息', - icon: 'none' - }); - return false; + if (blank(formData.companyName)) { + return missing('请填写企业名称'); + } + if (blank(formData.legalPerson)) { + return missing('请填写法定代表人'); + } + if (blank(formData.companyPhone)) { + return missing('请填写联系电话'); } if (this.hasPhoneError(formData.companyPhone, 'companyPhoneError')) { return false; } } - if (!formData.taskContent || !formData.needDays || !formData.totalCost) { - wx.showToast({ - title: '请填写完整的开发信息', - icon: 'none' - }); - return false; + if (blank(formData.taskContent)) { + return missing('请填写开发内容'); + } + if (emptyAmount(formData.totalCost)) { + return missing('请填写总费用'); + } + if (emptyAmount(formData.needDays)) { + return missing('请填写开发工期'); } if (this.data.attachFiles.some(item => item.status !== 'done')) { diff --git a/frontend/miniapp/pages/demand/detail/index.js b/frontend/miniapp/pages/demand/detail/index.js index 477653d..bcf0a8f 100644 --- a/frontend/miniapp/pages/demand/detail/index.js +++ b/frontend/miniapp/pages/demand/detail/index.js @@ -39,7 +39,16 @@ Page({ return new Promise((resolve) => { fetchDemandDetail({ id: id }).then( (res) => { - console.log(res); + if (!res || res.code !== 1 || !res.data) { + wx.showToast({ title: (res && res.msg) || '加载失败', icon: 'none' }); + if (!this.data.demandInfo) { + setTimeout(() => { + wx.navigateBack({ delta: 1 }); + }, 1500); + } + resolve(); + return; + } const demandInfo = res.data; this.setData({ demandInfo, @@ -47,7 +56,7 @@ Page({ }, () => resolve()); // setData 回调:页面已重新渲染 }, () => { - //接口异常处理 + wx.showToast({ title: '网络异常,请重试', icon: 'none' }); resolve(); }, ); diff --git a/frontend/miniapp/pages/demand/detail/index.wxml b/frontend/miniapp/pages/demand/detail/index.wxml index 867ebf0..bb683cf 100644 --- a/frontend/miniapp/pages/demand/detail/index.wxml +++ b/frontend/miniapp/pages/demand/detail/index.wxml @@ -42,7 +42,7 @@ 暂无附件 - + 报价 @@ -53,8 +53,8 @@ - - + + 开发完成凭证截图 diff --git a/frontend/miniapp/pages/demand/detail/index.wxs b/frontend/miniapp/pages/demand/detail/index.wxs index f82c005..3728148 100644 --- a/frontend/miniapp/pages/demand/detail/index.wxs +++ b/frontend/miniapp/pages/demand/detail/index.wxs @@ -82,6 +82,20 @@ var getDelivery = function (demandInfo) { return demandInfo.delivery || null; } +// 成交报价仅发布者和接单开发者可见;招标中其它开发者只看到自己的报价表单 +var canSeeQuote = function (demandInfo) { + if (!demandInfo) return false; + if (demandInfo.isMy || demandInfo.isMyWin) return true; + return demandInfo.status == 1; +} + +// 交付内容仅发布者和接单开发者可见 +var canSeeDelivery = function (demandInfo) { + if (!demandInfo) return false; + if (!demandInfo.isMy && !demandInfo.isMyWin) return false; + return !!getDelivery(demandInfo); +} + // 开发者待发货:需求待收货(客户已付尾款)、自己中标且尚未交付 var canDeliverGoods = function (demandInfo) { if (!demandInfo) return false; @@ -134,6 +148,8 @@ module.exports = { canConfirmReceipt: canConfirmReceipt, isWaitingDeliver: isWaitingDeliver, getDelivery: getDelivery, + canSeeQuote: canSeeQuote, + canSeeDelivery: canSeeDelivery, canDeliverGoods: canDeliverGoods, isDelivered: isDelivered, isDevView: isDevView, diff --git a/frontend/miniapp/pages/demand/list/index.js b/frontend/miniapp/pages/demand/list/index.js index 39b8e08..339ab1e 100644 --- a/frontend/miniapp/pages/demand/list/index.js +++ b/frontend/miniapp/pages/demand/list/index.js @@ -283,6 +283,14 @@ Page({ if (!item) { return; } + // 交易完成只允许发布者和接单开发者进入详情 + if (item.button && item.button.key === 'finished' && !item.isMy && !item.isMyWin) { + wx.showToast({ + title: '仅发布者和接单开发者可查看', + icon: 'none', + }); + return; + } // 参与投标仅开发者可进入报价;未入驻先提示,确定后再去申请 if (item.button && item.button.key === 'bid') { const role = (wx.getStorageSync('userInfo') || {}).role || ''; diff --git a/frontend/miniapp/pages/home/home.js b/frontend/miniapp/pages/home/home.js index 7d7b249..732673d 100644 --- a/frontend/miniapp/pages/home/home.js +++ b/frontend/miniapp/pages/home/home.js @@ -80,7 +80,7 @@ Page({ /** * 拉取最新角色并写回本地缓存 - * 入驻审核通过后,用户回到小程序触发 onShow 即可看到「进入接单大厅」入口 + * 未入驻显示「开发者入驻」;审核通过后回到首页即可看到「进入接单大厅」 */ refreshRole() { if (this._roleLoading) { @@ -165,6 +165,12 @@ Page({ }); }, + toDeveloperJoin() { + wx.navigateTo({ + url: '/pages/developer/join/index', + }); + }, + onBannerChange(e) { const current = Number((e.detail && e.detail.current) || 0); if (current === this.data.bannerCurrent) { diff --git a/frontend/miniapp/pages/home/home.wxml b/frontend/miniapp/pages/home/home.wxml index 6dfebc4..9795ee2 100644 --- a/frontend/miniapp/pages/home/home.wxml +++ b/frontend/miniapp/pages/home/home.wxml @@ -48,6 +48,11 @@ 进入接单大厅 + + + 开发者入驻 + + diff --git a/frontend/miniapp/pages/usercenter/components/order-group/index.js b/frontend/miniapp/pages/usercenter/components/order-group/index.js index 16b6b87..60f8c16 100644 --- a/frontend/miniapp/pages/usercenter/components/order-group/index.js +++ b/frontend/miniapp/pages/usercenter/components/order-group/index.js @@ -10,7 +10,7 @@ Component({ }, title: { type: String, - value: '我的支付订单', + value: '我的订单', }, desc: { type: String,