U8ThirdPartyService.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. <?php
  2. namespace App\Service;
  3. use App\Model\DDEmployee;
  4. use App\Model\PoPodetails;
  5. use App\Model\PoPomain;
  6. use App\Model\PuAppVouch;
  7. use App\Model\PuAppVouchs;
  8. use App\Model\RdRecord01;
  9. use App\Model\RdRecords01;
  10. use Illuminate\Support\Facades\Cache;
  11. use Illuminate\Support\Facades\Log;
  12. class U8ThirdPartyService extends Service
  13. {
  14. public function getToken($data){
  15. if(empty($data['database'])) return [false, '账套不能为空'];
  16. $key = "xky_u8_api" . $data['database'];
  17. $config = config('wms');
  18. // 1. 自检网络通畅
  19. list($bool, $msg) = $this->checkNetworkStatus($config['api_host'], $config['api_port']);
  20. if (!$bool) return [false, "网络连接失败: " . $msg];
  21. $host = $config['api_host'] . ":" . $config['api_port'];
  22. // 2. 尝试从缓存获取
  23. $token = Cache::get($key);
  24. if (! $token) {
  25. // 3. 缓存失效,请求新 Token
  26. $url = $host . "/api/System/GetToken";
  27. $date = date("Y-m-d");
  28. $json = [
  29. "U8DbName" => $data['database'],
  30. "sUserId" => $config['user_id'],
  31. "sPassword" => $config['user_password'],
  32. "LoginDateTime" => $date,
  33. "bPersist" => true
  34. ];
  35. $header = ['Content-Type:application/json'];
  36. // 调用你的 POST 辅助函数
  37. list($status, $result) = $this->post_helper1($url, json_encode($json), $header);
  38. if (!$status) return [false, "用友token获取失败: " . $result];
  39. if (!isset($result['code'])) return [false, '获取用友登录信息失败: 响应格式异常'];
  40. if ($result['code'] != 0) return [false, "U8错误: " . ($result['msg'] ?? '未知错误')];
  41. $token = $result['data']['Token'] ?? "";
  42. if (empty($token)) return [false, "接口返回的 Token 为空"];
  43. // 30分钟 = 1800秒
  44. Cache::put($key, $token, now()->addMinutes(30));
  45. }
  46. return [true, ['host' => $host, 'token' => $token]];
  47. }
  48. public function puAppVouch($record)
  49. {
  50. // 1. 查询本地请购单主表(严格匹配 MySQL 字段名)
  51. $order = PuAppVouch::where('del_time', 0)
  52. ->where('order_number', $record['order_number'])
  53. ->where('login_type', $record['login_type'])
  54. ->first();
  55. if (empty($order)) return [false, '请购单不存在或已被删除'];
  56. $order_array = $order->toArray();
  57. // 2. 查询本地请购单明细子表(多行数据)
  58. $details = PuAppVouchs::where('del_time', 0)
  59. ->where('main_id', $order_array['id'])
  60. ->get()->toArray();
  61. if (empty($details)) return [false, '请购单明细不存在或已被删除'];
  62. // 3. 获取用友 U8 接口的 Token
  63. list($status, $msg) = $this->getToken($record);
  64. if (! $status) return [false, $msg];
  65. // 4. 循环组装表体明细 (iBody)
  66. $iBody = [];
  67. foreach ($details as $index => $item) {
  68. $qty = (float)$item['quantity']; // 数量:quantity
  69. $taxUnitPrice = (float)$item['price']; // 原币含税单价:price
  70. $taxRate = (float)$item['rate']; // 税率:rate(例如 13 或 17)
  71. // --- 核心价格与金额计算逻辑 ---
  72. // 1. 原币价税合计 / 含税金额 (iOriSum)
  73. $iOriSum = round($qty * $taxUnitPrice, 2);
  74. // 2. 原币无税金额 / 金额 (iOriMoney)
  75. $iOriMoney = round($iOriSum / (1 + ($taxRate / 100)), 2);
  76. // 3. 原币税额 (iOriTaxPrice)
  77. $iOriTaxPrice = round($iOriSum - $iOriMoney, 2);
  78. // 4. 原币无税单价 (iOriCost)
  79. $iOriCost = round($taxUnitPrice / (1 + ($taxRate / 100)), 6);
  80. // 因为币种是人民币,汇率是 1,所以 本币价格 = 原币价格
  81. $fMoney = $iOriMoney; // 本币无税金额
  82. $iMoney = $iOriMoney; // 本币无税金额(对应你列出的 iMoney)
  83. $iTaxPrice = $iOriTaxPrice; // 本币税额
  84. $iBody[] = [
  85. "ivouchrowno" => $index + 1, // 行号且唯一
  86. "cInvCode" => (string)$item['cInvCode'], // 存货编码
  87. "fQuantity" => $qty, // fQuantity 数量
  88. "iPerTaxRate" => (int)$taxRate, // iPerTaxRate 税率
  89. "fNum" => 0, // 件数
  90. "dRequirDate" => $item['dRequirDate'], // 需求日期
  91. "dArriveDate" => $item['dArriveDate'], // 建议订货日期
  92. "cexch_name" => "人民币", // 币种
  93. "iExchRate" => 1, // 汇率
  94. // --- 补充的单价字段 ---
  95. "iOriTaxCost" => $taxUnitPrice, // iOriTaxCost 原币含税单价
  96. "fTaxPrice" => $taxUnitPrice, // fTaxPrice 含税单价
  97. "fUnitPrice" => $iOriCost, // fUnitPrice 无税单价
  98. "iOriCost" => $iOriCost, // iOriCost 原币单价
  99. // --- 补充的金额/税额字段 ---
  100. "fMoney" => $fMoney, // fMoney 金额 (本币无税金额)
  101. "iMoney" => $iMoney, // iMoney 本币金额
  102. "iOriMoney" => $iOriMoney, // iOriMoney 原币金额
  103. "iOriTaxPrice" => $iOriTaxPrice, // iOriTaxPrice 原币税额
  104. "iTaxPrice" => $iTaxPrice, // iTaxPrice 本币税额
  105. "iOriSum" => $iOriSum, // iOriSum 原币价税合计
  106. ];
  107. }
  108. // 5. 组织请购单请求数据结构
  109. $tmp = [
  110. "Inum" => "PuAppVouch",
  111. "Data" => [
  112. "iHead" => [
  113. "cBusType" => $order_array['cBusType'],
  114. "cPTCode" => $order_array['cPTCode'],
  115. "cMemo" => "接口生成",
  116. "IsVerify" => true,
  117. "ddate" => $order_array['order_time'],
  118. "PriceCalKey" => "iOriTaxCost",
  119. "cMaker" => DDEmployee::where('userid', $order_array['crt_id'])->where('login_type', $record['login_type'])->value('name'),
  120. ],
  121. "iBody" => $iBody
  122. ]
  123. ];
  124. $final_data = [$tmp];
  125. // 6. 准备发送请求
  126. $host = $msg['host'] ?? "";
  127. $token = $msg['token'] ?? "";
  128. $header = [
  129. "Authorization: {$token}",
  130. 'Content-Type: application/json'
  131. ];
  132. $url = rtrim($host, '/') . "/api/PuAppVouch/Add";
  133. $json = json_encode($final_data, JSON_UNESCAPED_UNICODE);
  134. // 7. 发送 POST 请求
  135. list($status, $result) = $this->post_helper1($url, $json, $header, 30);
  136. if (! $status) return [false, $result];
  137. if (! isset($result['code'])) return [false, '接口响应异常,请检查用友服务'];
  138. if ($result['code'] != 0) return [false, $result['msg']];
  139. // 8. 回写用友生成的单据编号
  140. $code = $result['data'][0]['VouchCode'];
  141. $order->code = $code;
  142. $order->save();
  143. return [true, ''];
  144. }
  145. public function poPomain($record)
  146. {
  147. // 1. 查询本地采购订单主表(对应 po_pomain 表)
  148. $order = PoPomain::where('del_time', 0)
  149. ->where('order_number', $record['order_number'])
  150. ->where('login_type', $record['login_type'])
  151. ->first();
  152. if (empty($order)) return [false, '采购订单主表记录不存在或已被删除'];
  153. $order_array = $order->toArray();
  154. // 2. 查询本地采购订单明细子表(对应 po_podetails 表)
  155. $details = PoPodetails::where('del_time', 0)
  156. ->where('main_id', $order_array['id'])
  157. ->get()->toArray();
  158. if (empty($details)) return [false, '采购订单明细不存在或已被删除'];
  159. // 3. 获取用友 U8 接口的 Token 和 Host 基础信息
  160. list($status, $msg) = $this->getToken($record);
  161. if (! $status) return [false, $msg];
  162. // 4. 循环组装采购订单表体明细 (iBody)
  163. $iBody = [];
  164. foreach ($details as $index => $item) {
  165. $qty = (float)$item['quantity']; // 数量:quantity
  166. $taxUnitPrice = (float)$item['price']; // 原币含税单价:price
  167. $taxRate = (float)$item['rate']; // 税率:rate(例如 13 或 17)
  168. $iAppIds = $item['iAppIds'] ?? null;
  169. // U8 采购订单要求的明细行基础结构
  170. $bodyRow = [
  171. "ivouchrowno" => $index + 1, // 行号且唯一
  172. "cInvCode" => $item['cInvCode'], // 存货编码
  173. "iQuantity" => $qty, // 数量
  174. "iPerTaxRate" => $taxRate, // 税率
  175. "iTaxPrice" => $taxUnitPrice, // 原币含税单价(与表头 PriceCalKey 对应)
  176. "dArriveDate" => $item['dArriveDate'], // 建议/计划到货日期
  177. "bGsp" => 0, // 是否质检,默认0
  178. "iAppIds" => $iAppIds,
  179. ];
  180. $iBody[] = $bodyRow;
  181. }
  182. $cappcode = null;
  183. if(! empty($order_array['cappcode'])) $cappcode = $order_array['cappcode'][0] ?? null;
  184. // 5. 组织采购订单请求的表头数据结构 (iHead)
  185. $iHead = [
  186. "cVenCode" => $order_array['cVenCode'], // 供应商编码
  187. "cDepCode" => $order_array['cDepCode'] ?? null, // 部门编码
  188. "iDiscountTaxType" => 0, // 0--应税外加(默认), 1--应税内含
  189. "iTaxRate" => (int)$order_array['iTaxRate'], // 税率
  190. "nflat" => (int)($order_array['nflat'] ?? 1), // 汇率
  191. "cexch_name" => !empty($order_array['cexch_name']) ? $order_array['cexch_name'] : "人民币", // 币种
  192. "IsVerify" => true, // 是否审核 (默认生成即审核)
  193. "cMemo" => !empty($order_array['mark']) ? $order_array['mark'] : "接口生成", // 备注
  194. "PriceCalKey" => "iTaxPrice", // 以含税单价作为计算基准
  195. "dPODate" => $order_array['order_time'], // 单据日期
  196. "cappcode" => $cappcode, // 请购单号
  197. "cMaker" => DDEmployee::where('userid', $order_array['crt_id'])->where('login_type', $record['login_type'])->value('name'),
  198. ];
  199. if(! empty($cappcode)) $iHead['cSource'] = 'app'; //参照的固定参数
  200. // 6. 包装成接口所需的嵌套数组格式
  201. $tmp = [
  202. "Inum" => "PurchaseOrder",
  203. "Data" => [
  204. "iHead" => $iHead,
  205. "iBody" => $iBody
  206. ]
  207. ];
  208. $final_data = [$tmp];
  209. // 7. 准备发送网络请求
  210. $host = $msg['host'] ?? "";
  211. $token = $msg['token'] ?? "";
  212. $header = [
  213. "Authorization: {$token}",
  214. 'Content-Type: application/json'
  215. ];
  216. $url = rtrim($host, '/') . "/api/PurchaseOrder/Add";
  217. $json = json_encode($final_data, JSON_UNESCAPED_UNICODE);
  218. // 8. 调用 POST 助手发送
  219. list($status, $result) = $this->post_helper1($url, $json, $header, 30);
  220. if (! $status) return [false, $result];
  221. if (! isset($result['code'])) return [false, '接口响应异常,请检查用友服务状态'];
  222. if ($result['code'] != 0) return [false, $result['msg']];
  223. // 9. 成功后回写用友生成的采购订单编号 (VouchCode)
  224. $u8VouchCode = $result['data'][0]['VouchCode'];
  225. $order->code = $u8VouchCode; // 回写到用友单据编号字段
  226. $order->save();
  227. return [true, ''];
  228. }
  229. public function purchaseInAdd($record)
  230. {
  231. // 1. 查询本地采购入库单主表(对应 rd_record01 表)
  232. $order = RdRecord01::where('del_time', 0)
  233. ->where('order_number', $record['order_number'])
  234. ->where('login_type', $record['login_type'])
  235. ->first();
  236. if (empty($order)) return [false, '采购入库单主表记录不存在或已被删除'];
  237. $order_array = $order->toArray();
  238. // 2. 查询本地采购入库单明细子表(对应 rd_records01 表)
  239. $details = RdRecords01::where('del_time', 0)
  240. ->where('main_id', $order_array['id'])
  241. ->get()->toArray();
  242. if (empty($details)) return [false, '采购入库单明细不存在或已被删除'];
  243. // 3. 获取用友 U8 接口的 Token 和 Host 基础信息
  244. list($status, $msg) = $this->getToken($record);
  245. if (!$status) return [false, $msg];
  246. // 4. 循环组装采购入库单表体明细 (iBody)
  247. $iBody = [];
  248. foreach ($details as $index => $item) {
  249. $qty = (float)$item['quantity'];
  250. $taxUnitPrice = (float)$item['price']; // 原币含税单价:price
  251. $taxRate = (float)$item['rate']; // 税率:rate(例如 13 或 17)
  252. // U8 采购入库单接口要求的明细行数据结构
  253. $bodyRow = [
  254. "iRowNo" => $index + 1, // 行号必填且唯一
  255. "cInvCode" => $item['cInvCode'], // 存货编码
  256. "cBatch" => $item['cBatch'] ?? null, // 批号(按需传入,默认空)
  257. "dMadeDate" => $item['dMadeDate'] ?? null, // 生产日期(按需传入,默认空)
  258. "dVDate" => $item['dVDate'] ?? null, // 失效日期(按需传入,默认空)
  259. "iTaxRate" => $taxRate, // 税率
  260. "iOriTaxCost" => $taxUnitPrice, // 原币含税单价(与表头 PriceCalKey 对应)
  261. "iinvexchrate" => 0, // 换算率,默认0
  262. "iQuantity" => $qty, // 实际入库数量(蓝单为正,红单为负)
  263. "iNum" => 0, // 辅计量件数,默认0
  264. "iNQuantity" => $qty, // 应收应发数量(默认等于实际入库数量)
  265. "iNNum" => 0, // 应收应发辅计量数量,默认0
  266. "iPOsID" => $item['iPOsID'] // 核心必填:采购订单子表唯一标识ID
  267. ];
  268. $iBody[] = $bodyRow;
  269. }
  270. $cOrderCode = null;
  271. if(! empty($order_array['cOrderCode'])) $cOrderCode = $order_array['cOrderCode'][0] ?? null;
  272. // 5. 组织采购入库单请求的表头数据结构 (iHead)
  273. $iHead = [
  274. "bIsRedVouch" => $order['bredvouch'] == 1 ? true : false , //是否红单
  275. "IsVerify" => true, // 是否审核
  276. "bCalPrice" => true, // 是否由接口自动计算金额 (方式一:拉取订单价格和税率)
  277. "PriceCalKey" => "iOriTaxCost", // 以含税单价作为计算基准
  278. "cWhCode" => $order_array['warehouseCode'], // 仓库编码
  279. "cPTCode" => $order_array['cPTCode'], // 采购类型编码
  280. "cRdCode" => $order_array['cRdCode'], // 出入库/收发类别编码
  281. "cVenCode" => $order_array['cVenCode'], // 供应商编码
  282. "cExch_Name" => !empty($order_array['cexch_name']) ? $order_array['cexch_name'] : "人民币", // 币种
  283. "iExchRate" => (int)($order_array['nflat'] ?? 1), // 汇率
  284. "iTaxRate" => (int)$order_array['iTaxRate'], // 税率
  285. "cDepCode" => $order_array['cDepCode'] ?? null, // 部门编码
  286. "cOrderCode" => $cOrderCode, // 关联的采购订单号
  287. "cBusType" => !empty($order_array['cBusType']) ? $order_array['cBusType'] : "普通采购", // 业务类型
  288. "cSource" => "采购订单", // 单据来源固定为采购订单
  289. "cMemo" => !empty($order_array['mark']) ? $order_array['mark'] : "API生成", // 备注
  290. "dDate" => $order_array['order_time'], // 单据日期
  291. "cMaker" => DDEmployee::where('userid', $order_array['crt_id'])->where('login_type', $record['login_type'])->value('name'),
  292. ];
  293. // 6. 包装成接口所需的嵌套外层数组格式
  294. $tmp = [
  295. "Inum" => "PurchaseIn",
  296. "Data" => [
  297. "iHead" => $iHead,
  298. "iBody" => $iBody
  299. ]
  300. ];
  301. $final_data = [$tmp];
  302. // 7. 准备发送网络请求
  303. $host = $msg['host'] ?? "";
  304. $token = $msg['token'] ?? "";
  305. $header = [
  306. "Authorization: {$token}",
  307. 'Content-Type: application/json'
  308. ];
  309. $url = rtrim($host, '/') . "/api/PurchaseIn/Add";
  310. $json = json_encode($final_data, JSON_UNESCAPED_UNICODE);
  311. // 8. 调用 POST 助手发送请求
  312. list($status, $result) = $this->post_helper1($url, $json, $header, 30);
  313. if (!$status) return [false, $result];
  314. if (!isset($result['code'])) return [false, '接口响应异常,请检查用友服务状态'];
  315. if ($result['code'] != 0) return [false, $result['msg']];
  316. // 9. 成功后回写用友生成的采购入库单单据编号 (VouchCode) 到本地 code
  317. // 注意:文档中返回的 data 也是一个包裹数组
  318. $u8VouchCode = $result['data'][0]['VouchCode'];
  319. $order->code = $u8VouchCode;
  320. $order->save();
  321. return [true, ''];
  322. }
  323. public function post_helper1($url, $data, $header = [], $timeout = 20){
  324. Log::channel('apiLog')->info('POST', ["api" => $url , "param" => json_decode($data,true) ,"header" => $header]);
  325. $ch = curl_init();
  326. curl_setopt($ch, CURLOPT_URL, $url);
  327. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  328. curl_setopt($ch, CURLOPT_ENCODING, '');
  329. curl_setopt($ch, CURLOPT_POST, 1);
  330. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  331. curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
  332. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  333. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  334. curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
  335. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
  336. if(!is_null($data)) curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  337. $r = curl_exec($ch);
  338. if ($r === false) {
  339. // 获取错误号
  340. $errorNumber = curl_errno($ch);
  341. // 获取错误信息
  342. $errorMessage = curl_error($ch);
  343. $message = "cURL Error #{$errorNumber}: {$errorMessage}";
  344. Log::channel('apiLog')->info('POST结果', ["message" => $message ]);
  345. return [false, $message];
  346. }
  347. curl_close($ch);
  348. $return = json_decode($r, true);
  349. unset($r);
  350. Log::channel('apiLog')->info('POST结果', ["message" => $return ]);
  351. return [true, $return];
  352. }
  353. }