U8ThirdPartyService.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  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. "cDepCode" => $order_array['departmentCode'] ?? null,
  116. "cMemo" => "接口生成",
  117. "IsVerify" => true,
  118. "ddate" => $order_array['order_time'],
  119. "PriceCalKey" => "iOriTaxCost",
  120. "cMaker" => DDEmployee::where('userid', $order_array['crt_id'])->where('login_type', $record['login_type'])->value('name'),
  121. ],
  122. "iBody" => $iBody
  123. ]
  124. ];
  125. $final_data = [$tmp];
  126. // 6. 准备发送请求
  127. $host = $msg['host'] ?? "";
  128. $token = $msg['token'] ?? "";
  129. $header = [
  130. "Authorization: {$token}",
  131. 'Content-Type: application/json'
  132. ];
  133. $url = rtrim($host, '/') . "/api/PuAppVouch/Add";
  134. $json = json_encode($final_data, JSON_UNESCAPED_UNICODE);
  135. // 7. 发送 POST 请求
  136. list($status, $result) = $this->post_helper1($url, $json, $header, 40);
  137. if (! $status) return [false, $result];
  138. if (! isset($result['code'])) return [false, '接口响应异常,请检查用友服务'];
  139. if ($result['code'] != 0) return [false, $result['msg']];
  140. // 8. 回写用友生成的单据编号
  141. $code = $result['data'][0]['VouchCode'];
  142. $order->code = $code;
  143. $order->save();
  144. return [true, ''];
  145. }
  146. public function poPomain($record)
  147. {
  148. // 1. 查询本地采购订单主表(对应 po_pomain 表)
  149. $order = PoPomain::where('del_time', 0)
  150. ->where('order_number', $record['order_number'])
  151. ->where('login_type', $record['login_type'])
  152. ->first();
  153. if (empty($order)) return [false, '采购订单主表记录不存在或已被删除'];
  154. $order_array = $order->toArray();
  155. // 2. 查询本地采购订单明细子表(对应 po_podetails 表)
  156. $details = PoPodetails::where('del_time', 0)
  157. ->where('main_id', $order_array['id'])
  158. ->get()->toArray();
  159. if (empty($details)) return [false, '采购订单明细不存在或已被删除'];
  160. // 3. 获取用友 U8 接口的 Token 和 Host 基础信息
  161. list($status, $msg) = $this->getToken($record);
  162. if (! $status) return [false, $msg];
  163. // 4. 循环组装采购订单表体明细 (iBody)
  164. $iBody = [];
  165. foreach ($details as $index => $item) {
  166. $qty = (float)$item['quantity']; // 数量:quantity
  167. $taxUnitPrice = (float)$item['price']; // 原币含税单价:price
  168. $taxRate = (float)$item['rate']; // 税率:rate(例如 13 或 17)
  169. $iAppIds = $item['iAppIds'] ?? null;
  170. // U8 采购订单要求的明细行基础结构
  171. $bodyRow = [
  172. "ivouchrowno" => $index + 1, // 行号且唯一
  173. "cInvCode" => $item['cInvCode'], // 存货编码
  174. "iQuantity" => $qty, // 数量
  175. "iPerTaxRate" => $taxRate, // 税率
  176. "iTaxPrice" => $taxUnitPrice, // 原币含税单价(与表头 PriceCalKey 对应)
  177. "dArriveDate" => $item['dArriveDate'], // 建议/计划到货日期
  178. "bGsp" => 0, // 是否质检,默认0
  179. "iAppIds" => $iAppIds,
  180. ];
  181. $iBody[] = $bodyRow;
  182. }
  183. $cappcode = null;
  184. if(! empty($order_array['cappcode'])) $cappcode = $order_array['cappcode'][0] ?? null;
  185. // 5. 组织采购订单请求的表头数据结构 (iHead)
  186. $iHead = [
  187. "cVenCode" => $order_array['cVenCode'], // 供应商编码
  188. "cDepCode" => $order_array['cDepCode'] ?? null, // 部门编码
  189. "iDiscountTaxType" => 0, // 0--应税外加(默认), 1--应税内含
  190. "iTaxRate" => (int)$order_array['iTaxRate'], // 税率
  191. "nflat" => (int)($order_array['nflat'] ?? 1), // 汇率
  192. "cexch_name" => !empty($order_array['cexch_name']) ? $order_array['cexch_name'] : "人民币", // 币种
  193. "IsVerify" => true, // 是否审核 (默认生成即审核)
  194. "cMemo" => !empty($order_array['mark']) ? $order_array['mark'] : "接口生成", // 备注
  195. "PriceCalKey" => "iTaxPrice", // 以含税单价作为计算基准
  196. "dPODate" => $order_array['order_time'], // 单据日期
  197. "cappcode" => $cappcode, // 请购单号
  198. "cMaker" => DDEmployee::where('userid', $order_array['crt_id'])->where('login_type', $record['login_type'])->value('name'),
  199. ];
  200. if(! empty($cappcode)) $iHead['cSource'] = 'app'; //参照的固定参数
  201. // 6. 包装成接口所需的嵌套数组格式
  202. $tmp = [
  203. "Inum" => "PurchaseOrder",
  204. "Data" => [
  205. "iHead" => $iHead,
  206. "iBody" => $iBody
  207. ]
  208. ];
  209. $final_data = [$tmp];
  210. // 7. 准备发送网络请求
  211. $host = $msg['host'] ?? "";
  212. $token = $msg['token'] ?? "";
  213. $header = [
  214. "Authorization: {$token}",
  215. 'Content-Type: application/json'
  216. ];
  217. $url = rtrim($host, '/') . "/api/PurchaseOrder/Add";
  218. $json = json_encode($final_data, JSON_UNESCAPED_UNICODE);
  219. // 8. 调用 POST 助手发送
  220. list($status, $result) = $this->post_helper1($url, $json, $header, 40);
  221. if (! $status) return [false, $result];
  222. if (! isset($result['code'])) return [false, '接口响应异常,请检查用友服务状态'];
  223. if ($result['code'] != 0) return [false, $result['msg']];
  224. // 9. 成功后回写用友生成的采购订单编号 (VouchCode)
  225. $u8VouchCode = $result['data'][0]['VouchCode'];
  226. $order->code = $u8VouchCode; // 回写到用友单据编号字段
  227. $order->save();
  228. return [true, ''];
  229. }
  230. public function purchaseInAdd($record)
  231. {
  232. // 1. 查询本地采购入库单主表(对应 rd_record01 表)
  233. $order = RdRecord01::where('del_time', 0)
  234. ->where('order_number', $record['order_number'])
  235. ->where('login_type', $record['login_type'])
  236. ->first();
  237. if (empty($order)) return [false, '采购入库单主表记录不存在或已被删除'];
  238. $order_array = $order->toArray();
  239. // 2. 查询本地采购入库单明细子表(对应 rd_records01 表)
  240. $details = RdRecords01::where('del_time', 0)
  241. ->where('main_id', $order_array['id'])
  242. ->get()->toArray();
  243. if (empty($details)) return [false, '采购入库单明细不存在或已被删除'];
  244. // 3. 获取用友 U8 接口的 Token 和 Host 基础信息
  245. list($status, $msg) = $this->getToken($record);
  246. if (!$status) return [false, $msg];
  247. // 4. 循环组装采购入库单表体明细 (iBody)
  248. $iBody = [];
  249. foreach ($details as $index => $item) {
  250. $qty = (float)$item['quantity'];
  251. $taxUnitPrice = (float)$item['price']; // 原币含税单价:price
  252. $taxRate = (float)$item['rate']; // 税率:rate(例如 13 或 17)
  253. // U8 采购入库单接口要求的明细行数据结构
  254. $bodyRow = [
  255. "iRowNo" => $index + 1, // 行号必填且唯一
  256. "cInvCode" => $item['cInvCode'], // 存货编码
  257. "cBatch" => $item['cBatch'] ?? null, // 批号(按需传入,默认空)
  258. "dMadeDate" => $item['dMadeDate'] ?? null, // 生产日期(按需传入,默认空)
  259. "dVDate" => $item['dVDate'] ?? null, // 失效日期(按需传入,默认空)
  260. "iTaxRate" => $taxRate, // 税率
  261. "iOriTaxCost" => $taxUnitPrice, // 原币含税单价(与表头 PriceCalKey 对应)
  262. "iinvexchrate" => 0, // 换算率,默认0
  263. "iQuantity" => $qty, // 实际入库数量(蓝单为正,红单为负)
  264. "iNum" => 0, // 辅计量件数,默认0
  265. "iNQuantity" => $qty, // 应收应发数量(默认等于实际入库数量)
  266. "iNNum" => 0, // 应收应发辅计量数量,默认0
  267. "iPOsID" => $item['iPOsID'] // 核心必填:采购订单子表唯一标识ID
  268. ];
  269. $iBody[] = $bodyRow;
  270. }
  271. $cOrderCode = null;
  272. if(! empty($order_array['cOrderCode'])) $cOrderCode = $order_array['cOrderCode'][0] ?? null;
  273. // 5. 组织采购入库单请求的表头数据结构 (iHead)
  274. $iHead = [
  275. "bIsRedVouch" => $order_array['bredvouch'] == 1 ? true : false , //是否红单
  276. "IsVerify" => true, // 是否审核
  277. "bCalPrice" => true, // 是否由接口自动计算金额 (方式一:拉取订单价格和税率)
  278. "PriceCalKey" => "iOriTaxCost", // 以含税单价作为计算基准
  279. "cWhCode" => $order_array['warehouseCode'], // 仓库编码
  280. "cPTCode" => $order_array['cPTCode'], // 采购类型编码
  281. "cRdCode" => $order_array['cRdCode'], // 出入库/收发类别编码
  282. "cVenCode" => $order_array['cVenCode'], // 供应商编码
  283. "cExch_Name" => !empty($order_array['cexch_name']) ? $order_array['cexch_name'] : "人民币", // 币种
  284. "iExchRate" => (int)($order_array['nflat'] ?? 1), // 汇率
  285. "iTaxRate" => (int)$order_array['iTaxRate'], // 税率
  286. "cDepCode" => $order_array['cDepCode'] ?? null, // 部门编码
  287. "cOrderCode" => $cOrderCode, // 关联的采购订单号
  288. "cBusType" => !empty($order_array['cBusType']) ? $order_array['cBusType'] : "普通采购", // 业务类型
  289. "cSource" => "采购订单", // 单据来源固定为采购订单
  290. "cMemo" => !empty($order_array['mark']) ? $order_array['mark'] : "API生成", // 备注
  291. "dDate" => $order_array['order_time'], // 单据日期
  292. "cMaker" => DDEmployee::where('userid', $order_array['crt_id'])->where('login_type', $record['login_type'])->value('name'),
  293. ];
  294. // 6. 包装成接口所需的嵌套外层数组格式
  295. $tmp = [
  296. "Inum" => "PurchaseIn",
  297. "Data" => [
  298. "iHead" => $iHead,
  299. "iBody" => $iBody
  300. ]
  301. ];
  302. $final_data = [$tmp];
  303. // 7. 准备发送网络请求
  304. $host = $msg['host'] ?? "";
  305. $token = $msg['token'] ?? "";
  306. $header = [
  307. "Authorization: {$token}",
  308. 'Content-Type: application/json'
  309. ];
  310. $url = rtrim($host, '/') . "/api/PurchaseIn/Add";
  311. $json = json_encode($final_data, JSON_UNESCAPED_UNICODE);
  312. // 8. 调用 POST 助手发送请求
  313. list($status, $result) = $this->post_helper1($url, $json, $header, 40);
  314. if (!$status) return [false, $result];
  315. if (!isset($result['code'])) return [false, '接口响应异常,请检查用友服务状态'];
  316. if ($result['code'] != 0) return [false, $result['msg']];
  317. // 9. 成功后回写用友生成的采购入库单单据编号 (VouchCode) 到本地 code
  318. // 注意:文档中返回的 data 也是一个包裹数组
  319. $u8VouchCode = $result['data'][0]['VouchCode'];
  320. $order->code = $u8VouchCode;
  321. $order->save();
  322. return [true, ''];
  323. }
  324. public function post_helper1($url, $data, $header = [], $timeout = 20){
  325. Log::channel('apiLog')->info('POST', ["api" => $url , "param" => json_decode($data,true) ,"header" => $header]);
  326. $ch = curl_init();
  327. curl_setopt($ch, CURLOPT_URL, $url);
  328. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  329. curl_setopt($ch, CURLOPT_ENCODING, '');
  330. curl_setopt($ch, CURLOPT_POST, 1);
  331. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  332. curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
  333. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  334. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  335. curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
  336. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
  337. if(!is_null($data)) curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  338. $r = curl_exec($ch);
  339. if ($r === false) {
  340. // 获取错误号
  341. $errorNumber = curl_errno($ch);
  342. // 获取错误信息
  343. $errorMessage = curl_error($ch);
  344. $message = "cURL Error #{$errorNumber}: {$errorMessage}";
  345. Log::channel('apiLog')->info('POST结果', ["message" => $message ]);
  346. return [false, $message];
  347. }
  348. curl_close($ch);
  349. $return = json_decode($r, true);
  350. unset($r);
  351. Log::channel('apiLog')->info('POST结果', ["message" => $return ]);
  352. return [true, $return];
  353. }
  354. }