| 学习交流 » 使用经验 »AI编写提取订单明细和出货数量,上机信息的自定义报表脚本,DLL文件(39519条10秒完成)
|
|
| |
| 作者 |
|
fwj3861 [个人空间] QQ名 越南-叶子(121305301)
    注册  2010-09-09 发贴数 641 精华贴 10 原创贴 20 来自 越南 状态 正常
级别 贵宾 |
|
|
由于客户订单明细比较多,从3月1日到7月31号的5个月共有39519条订单记录,在官方的单据明细报表里面去提取这些数据要10多分钟,每天所有业务员都会要去提取订单明细好几次,导出或查看销售出货进度,严重影响了其他客户端制单,所以另寻途径,让AI做一个自定义报表,并做一个DLL文件直接从数据库提取明细写入到EXCEL文档,经过不断优化,生成EXCEL文档并打开从最初用时2分钟提升到10秒即可完成。 思路: 1. 绕开客户端提交事务给服务端的方式,客户端只负责提交检索条件组合给DLL文件 2. 数据库新建一个只读账号,做成数据库连接配置文档,DLL文件读取这个配置文档去从数据库提取数据 3. 外网登录客户端使用VPN连接,限定只能通过内网IP去访问数据库,不做数据库的端口映射,提高安全性 4. DLL文件和报表脚本文件都增加版本号,并上传到文档管理,提取数据前先检查客户端是否存在DLL文件或是否版本号跟文档管理的一致,若不存在或不一致,则弹出更新菜单,点击是自动从文档管理下载覆盖更新到客户端dll目录,并弹出重新登陆框,必须重新登陆才能确保读取的是最新版本的文件(数据库连接配置文件自动下载到DLL目录),使用在线更新的方式方便后期维护升级DLL文件 先发CPP文件 》》》》》》 // customer_order_ship_detail_export.cpp // 客户订单出货明细 Excel 导出 DLL -- 供 4GL 脚本引擎通过 call_user_dll 调用 // // V8(2026081201):彻底重写,不再使用WPS/Excel COM自动化写文件--V5~V7反复 // 测试证实,单文件写到10500~11400行区间必崩(External exception EEFFACE/ // Access violation),是COM自动化链路本身(32位进程)的资源天花板,不是 // 具体某段代码的bug。V33曾经改成"超过8000条直接拒绝导出",被明确否决: // 必须支持任意行数的单一文件。V34改成多文件自动拆分,也被否决:必须只 // 有一份文件。这次改成完全不依赖WPS/Excel进程的方案--直接读写.xlsx // 的底层格式(.xlsx本质是一个zip包,里面是若干.xml文件)。具体做法: // 1) 模版(客户订单明细表导出模版.xlsx)本身不用整个解压/重新压缩-- // 除了xl/worksheets/sheet1.xml(存实际内容那张表)之外的其余13个 // zip条目(样式/共享字符串/图片/主题/打印设置等)原始压缩字节整体 // 原样拷贝到新文件里,不解压也不重新压缩,彻底不需要deflate/inflate // 算法,不依赖任何第三方压缩库(miniz等第三方库在32位MSVC下的构建 // 风险、需要额外安装CMake/vcpkg的成本都不小,权衡后放弃) // 2) 只有sheet1.xml这一个条目整个重新生成,用最简单的STORED(不压缩) // 方式写入zip,只需要实现CRC32(标准算法,20行左右)和zip文件格式 // 本身(本地文件头+中央目录+结尾记录,是有文档的固定二进制格式), // 不需要压缩 // 3) 表头1-9行(标题+列名,合并单元格/字体/颜色等样式)从模版原始xml // 里摘出来的固定内容硬编码在本文件里(下方h1~h22的22个值对应的 // 具体单元格坐标,是从模版xl/worksheets/sheet1.xml反查出来的, // 跟V1~V7脚本注释里那份坐标表完全一致),明细行(第10行起)和合计行 // 按实际条数动态生成--这样彻底不受"COM单次调用/表格总行数崩溃 // 上限"的限制,内存占用只跟文本大小成正比(几万行也就几十MB字符串), // 没有已知的行数天花板 // 4) 生成的.xlsx体积会比原来COM方式略大(因为sheet1.xml这部分改成不 // 压缩存储),对于几千到几万行的报表数据量级,这个体积增加可以忽略 // // INIT/APPEND/FINAL三阶段协议对脚本那边完全不变(中间文件格式、 // call_user_dll调用方式、返回值前缀"init ok"/"append ok"/"final ok" // 全部保持一致,4GL脚本不需要跟着改动多少)。因为每次call_user_dll调用 // DLL都是独立的(没有跨调用保留内存状态),内部实现改成用两个临时文件 // 做"状态持久化": // INIT 校验模版文件能打开,把22个筛选值+模版路径+总行数写进一个 // 小的meta临时文件(outPath+".meta.tmp"),另建一个空的行数据 // 累积临时文件(outPath+".rows.tmp") // APPEND 把这一批明细行原样追加写进rows累积临时文件,不做任何xml/ // zip相关的重活,单纯文本追加,非常快 // FINAL 读回meta+全部累积的行数据,在内存里拼出完整的sheet1.xml // 文本,跟模版zip"克隆"成最终的.xlsx文件,删掉两个临时文件 // // 导出函数固定格式(脚本引擎要求,不变): // extern "C" __declspec(dllexport) int export_customer_order_ship_detail_excel(char* para, char* ret) // para = 中间数据文件的完整路径 // ret = 调用方预分配的返回缓冲区,最多 500 字节 // // 编译(32位,跟宿主ERP程序一致)--不再需要ole32.lib/oleaut32.lib, // 也不需要任何第三方库/CMake,一行搞定: // 用 "x86 Native Tools Command Prompt for VS" 打开命令行,cd 到本文件目录,执行: // cl /LD /EHsc /utf-8 customer_order_ship_detail_export.cpp /Fe:customer_order_ship_detail_export.dll // // 导出模版布局(客户订单明细表导出模版.xlsx,40列A-AN)--跟V1~V7完全一致: // row1-3 公司名/报表标题(模版自带,原样透传) // row4 开始日期(D4) 结束日期(L4) 客户(R4) 内部订号(W4) // 内部物料编号(AB4) 客户物料代码(AI4) // row5 工单号(C5) 预告工单号(L5,一般为空) 客户采购单号(S5) 客户指令(Z5) // row6 客户型体(C6) 通用色号(S6) 内部色号(AI6) // row7 是否打确认样(E7) 是否防虹吸(K7) 订购胶片(P7) 织带印刷(V7) // 客户订单备注(AA7) 明细备注(AI7) // row8 品名描述(D8) 订单单位(AI8) 出货状态(AM8) // row9 列标题(模版自带,原样透传) // row10..(9+N) N行明细数据(动态生成) // row(10+N) 查询人(E:J合并)/查询日期(P:U合并)/合计(AH/AI/AJ/AN四个 // 数字列各自单独写合计,不合并) // 明细列顺序(A..AN共40列): 序号,订单单据编号,单据日期,制单人,单据状态, // 审核人,客户编号,业务员,内部订号,订单类型,生产部门,成品项次,工单号, // 预告工单号,下单日期,交付日期,客户采购单号,客户型体,项次,客户指令, // 企划单号,企业单号,客户物料编号,内部物料编号,品名描述,规格,通用色号, // 内部色号,长度,是否打确认样,是否防虹吸,订购胶片,织带印刷,订单数量, // 已出货数量,待出货数量,订单单位,客户订单备注,备注,生产数量(Y) #include <string> #include <regex> #include <set> #include <vector> #include <fstream> #include <sstream> #include <cstdint> #include <cstring> #include <cstdio> #ifdef _WIN32 #include <windows.h> #include <sql.h> #include <sqlext.h> #endif // V9(2026081202):修复"Unicode物理路径乱码导致INIT阶段打不开模版文件"的报错 // (现场报错:导出失败:INIT阶段失败:模版文件打开失败, path=D:\...\<中文>.xlsx, // 且路径与报错文字全是乱码)。根因是编码在DLL边界处不统一: // - 入口把脚本传来的GBK中间文件整体转成了UTF-8,于是tplPath/outPath // 都是UTF-8字节串;但打开文件用的是窄字符std::ifstream/ofstream, // Windows下窄字符文件流按系统ACP(GBK)解释路径 -> UTF-8路径被当GBK // 读 -> 中文路径找不到文件 -> INIT失败 // - 返回给脚本的ret也是UTF-8,脚本按GBK显示 -> 报错文字乱码 // 修法(两处边界各自归一,核心逻辑一行不动): // 1) 所有"内部UTF-8路径"打开文件时,统一UTF-8->宽字符,按宽路径打开 // (OpenIn/OpenOut/RemovePath);para(脚本直传的ACP路径)按ACP->宽打开 // 2) 写回ret时统一UTF-8->GBK(SafeCopyRet内部转),脚本侧显示正常 // 注意:版本号从2026081201改到2026081202,记得把脚本里的 // EXPECTED_COSD_DLL_VER常量一起同步改成2026081202,两边一致才不会触发 // 脚本侧"版本不一致,自动更新DLL"的逻辑。 static const char* DLL_VERSION = "direct-2026081320"; // ==================================================================== // 0. 小工具 // ==================================================================== // ---- Unicode 路径 / 返回值编码适配层 ---- // 内部所有字符串都是UTF-8;跟操作系统打交道(打开文件、写回ret)时才在 // 这里做一次编码转换。非Windows(本地测试编译)下路径本来就是UTF-8,直接 // 用窄字符打开即可,ret也不用转。 #ifdef _WIN32 static std::wstring Utf8ToWide(const std::string& s) { if (s.empty()) return L""; int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), NULL, 0); std::wstring w(n, 0); MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &w[0], n); return w; } static std::wstring AnsiToWide(const std::string& s) // para等ACP(GBK)字节串用 { if (s.empty()) return L""; int n = MultiByteToWideChar(CP_ACP, 0, s.c_str(), (int)s.size(), NULL, 0); std::wstring w(n, 0); MultiByteToWideChar(CP_ACP, 0, s.c_str(), (int)s.size(), &w[0], n); return w; } static std::string Utf8ToAnsi(const std::string& s) // 写回ret前UTF-8->GBK { if (s.empty()) return ""; std::wstring w = Utf8ToWide(s); int n = WideCharToMultiByte(CP_ACP, 0, w.c_str(), (int)w.size(), NULL, 0, NULL, NULL); std::string a(n, 0); WideCharToMultiByte(CP_ACP, 0, w.c_str(), (int)w.size(), &a[0], n, NULL, NULL); return a; } static void OpenIn(std::ifstream& f, const std::string& utf8path) { f.open(Utf8ToWide(utf8path).c_str(), std::ios::binary); } static void OpenOut(std::ofstream& f, const std::string& utf8path, std::ios::openmode m) { f.open(Utf8ToWide(utf8path).c_str(), std::ios::binary | m); } static void RemovePath(const std::string& utf8path) { _wremove(Utf8ToWide(utf8path).c_str()); } #else static void OpenIn(std::ifstream& f, const std::string& path) { f.open(path.c_str(), std::ios::binary); } static void OpenOut(std::ofstream& f, const std::string& path, std::ios::openmode m) { f.open(path.c_str(), std::ios::binary | m); } static void RemovePath(const std::string& path) { std::remove(path.c_str()); } #endif // 往调用方预分配的ret缓冲区(500字节)安全写字符串,不用strcpy_s(那是 // MSVC专属,这里为了能在非Windows环境下也编译通过做测试,自己实现)。 // Windows下统一把UTF-8内部字符串转成GBK再写回,否则脚本按GBK显示会乱码。 static void SafeCopyRet(char* ret, size_t cap, const std::string& s) { if (!ret || cap == 0) return; #ifdef _WIN32 std::string o = Utf8ToAnsi(s); #else const std::string& o = s; #endif size_t n = o.size(); if (n > cap - 1) n = cap - 1; memcpy(ret, o.data(), n); ret[n] = '\0'; } // ==================================================================== // 1. CRC32(标准算法,多项式0xEDB88320,zip格式要求) // ==================================================================== static uint32_t g_crcTable[256]; static bool g_crcTableReady = false; static void InitCrc32Table() { for (uint32_t i = 0; i < 256; i++) { uint32_t c = i; for (int k = 0; k < 8; k++) { c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1); } g_crcTable[i] = c; } g_crcTableReady = true; } static uint32_t Crc32(const unsigned char* data, size_t len) { if (!g_crcTableReady) InitCrc32Table(); uint32_t c = 0xFFFFFFFFu; for (size_t i =0; i < len; i++) { c = g_crcTable[(c ^ data[i]) & 0xFF] ^ (c >> 8); } return c ^ 0xFFFFFFFFu; } static uint32_t Crc32(const std::string& s) { return Crc32((const unsigned char*)s.data(), s.size()); } // 增量CRC32:从running crc继续往下累加,不做首尾异或(由调用方在开头置 // 0xFFFFFFFF、结尾再异或0xFFFFFFFF)。流式写sheet1.xml时边写边算,不需要 // 把整张表内容留在内存里 static uint32_t Crc32Cont(uint32_t crc, const unsigned char* data, size_t len) { if (!g_crcTableReady) InitCrc32Table(); for (size_t i = 0; i < len; i++) crc = g_crcTable[(crc ^ data[i]) & 0xFF] ^ (crc >> 8); return crc; } // ==================================================================== // 2. Excel日期序列号(自1899-12-30起的天数,跟Excel内部日期存储一致)。 // 用Howard Hinnant的civil_from_days标准算法(公历日期转"距1970-01-01 // 的天数"),再减去1899-12-30对应的天数(-25569)得到Excel序列号。 // 已用模版自带样例校准过:2026-08-11 == 46245(模版D4原始示例值就是 // 这个数字,算法结果完全一致) // ==================================================================== static long long DaysFromCivil(int y, int m, int d) { y -= (m <= 2) ? 1 : 0; long long era = (y >= 0 ? y : y - 399) / 400; unsigned yoe = (unsigned)(y - era * 400); unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; return era * 146097 + (long long)doe - 719468; } static long ExcelSerialFromYmd(int y, int m, int d) { return (long)(DaysFromCivil(y, m, d) + 25569); } // 解析"yyyy-mm-dd"格式的日期字符串,失败返回false(保持原样当文本处理) static bool ParseYmdDash(const std::string& s, int& y, int& m, int& d) { if (s.size() < 10) return false; if (s[4] != '-' || s[7] != '-') return false; for (int i = 0; i < 10; i++) { if (i == 4 || i == 7) continue; if (s[i] < '0' || s[i] > '9') return false; } y = atoi(s.substr(0, 4).c_str()); m = atoi(s.substr(5,2).c_str()); d = atoi(s.substr(8, 2).c_str()); return true; } // h1/h2(开始/结束日期)优先转成Excel日期序列号,写成真正的日期类型 // (配合模版D4/L4已有的日期数字格式样式s=13,显示效果跟老版本COM写入 // 一致);解析失败(比如为空)就退化成空字符串,写空的日期值单元格 static std::string DateStrToSerialOrEmpty(const std::string& s) { int y, m, d; if (!ParseYmdDash(s, y, m, d)) return ""; long serial = ExcelSerialFromYmd(y, m, d); if (serial <= 0) return ""; char buf[32]; snprintf(buf, sizeof(buf), "%ld", serial); return std::string(buf); } // ==================================================================== // 3. XML文本转义(&,<,>)--纯字节级扫描,UTF-8多字节字符的后续字节 // 都是>=0x80,不会跟这几个ASCII符号混淆,按字节扫描是安全的 // ==================================================================== static std::string XmlEscape(const std::string& s) { std::string out; out.reserve(s.size() + 8); for (char ch : s) { switch (ch) { case '&': out += "&"; break; case '<': out += "<"; break; case '>': out += ">"; break; default: out += ch; break; } } return out; } // 生成一个内联文本单元格 <c r="A1" s="8" t="inlineStr"><is><t xml:space="preserve">...</t></is></c> static std::string CellInlineStr(const std::string& ref, const std::string& style, const std::string& text) { std::string out = "<c r=\"" + ref + "\" s=\"" + style + "\""; if (text.empty()) { out += "/>"; return out; } out += " t=\"inlineStr\"><is><t xml:space=\"preserve\">" + XmlEscape(text) + "</t></is></c>"; return out; } // 生成一个数字单元格(空文本按0处理,避免合计/明细数字列出现空值报错) static std::string CellNum(const std::string& ref, const std::string& style, const std::string& numText) { std::string v = numText.empty() ? "0" : numText; // 防御性处理:数字字段里混进非数字字符(理论上不会,SQL SUM/明细 // 数字列本来就是数字),这种情况写0,避免生成非法xml数字值 bool valid = !v.empty(); for (char ch : v) { if (!((ch >= '0' && ch <= '9') || ch == '-' || ch == '.')) { valid = false; break; } } if (!valid) v = "0"; return "<c r=\"" + ref + "\" s=\"" + style + "\"><v>" + v + "</v></c>"; } static std::string ColLetter(int col1based) { std::string s; int col = col1based; while (col > 0) { int rem = (col - 1) % 26; s = std::string(1, (char)('A' + rem)) + s; col = (col - 1) / 26; } return s; } // ==================================================================== // 4. ExportJob:中间数据文件解析(跟老版本完全一样的协议格式,只是把 // std::wstring换成std::string--统一在DLL入口处就把GBK转成UTF-8,内部 // 全部按UTF-8字节串处理,不再需要宽字符) // ==================================================================== struct ExportJob { std::string mode; std::string tplPath, outPath; int totalCounts = 0; int startOffset = 0; int chunkCount = 0; std::vector<std::string> header; // INIT: 22个筛选值 std::vector<std::vector<std::string>> rows; // APPEND: 每行40个字段 std::vector<std::string> totals; // FINAL: 查询人/查询日期+4个数字合计 }; static const std::string FIELD_DELIM = "|~|"; static std::vector<std::string> SplitUS(const std::string& line) { std::vector<std::string> out; size_t pos = 0, found; while ((found = line.find(FIELD_DELIM, pos)) != std::string::npos) { out.push_back(line.substr(pos, found - pos)); pos = found + FIELD_DELIM.size(); } out.push_back(line.substr(pos)); return out; } static std::string JoinUS(const std::vector<std::string>& fields) { std::string out; for (size_t i = 0; i < fields.size(); i++) { if (i) out += FIELD_DELIM; out += fields[i]; } return out; } static std::vector<std::string> SplitLines(const std::string& text) { std::vector<std::string> lines; std::string cur; for (char c : text) { if (c == '\n') { lines.push_back(cur); cur.clear(); } else if (c != '\r') cur.push_back(c); } if (!cur.empty()) lines.push_back(cur); return lines; } static bool LoadJob(const std::string& utf8Text, ExportJob& job, std::string& err) { std::vector<std::string> lines = SplitLines(utf8Text); if (lines.empty()) { err = "intermediate file empty"; return false; } auto l0 = SplitUS(lines[0]); if (l0.empty()) { err = "line1 empty"; return false; } job.mode = l0[0]; if (job.mode == "INIT") { if (l0.size() < 4) { err = "INIT line1 field count wrong"; return false; } job.tplPath = l0[1]; job.outPath = l0[2]; job.totalCounts = atoi(l0[3].c_str()); if (lines.size() > 1) job.header = SplitUS(lines[1]); } else if (job.mode == "APPEND") { if (l0.size() < 4) { err = "APPEND line1 field count wrong"; return false; } job.outPath = l0[1]; job.startOffset = atoi(l0[2].c_str()); job.chunkCount = atoi(l0[3].c_str()); for (int i = 0; i < job.chunkCount; i++) { size_t idx = 1 + (size_t)i; if (idx >= lines.size()) { err = "APPEND row data missing"; return false; } job.rows.push_back(SplitUS(lines[idx])); } } else if (job.mode == "FINAL") { if (l0.size() < 3) { err = "FINAL line1 field count wrong"; return false; } job.outPath = l0[1]; job.totalCounts = atoi(l0[2].c_str()); if (lines.size() > 1) job.totals = SplitUS(lines[1]); } else { err = "unknown mode"; return false; } return true; } // ==================================================================== // 5. 极简zip读写:只支持我们这里需要的场景--"打开一个现成的zip // (模版.xlsx),除了一个条目外其余原始压缩字节整体拷贝,那一个条目 // 用STORED(不压缩)方式写入新内容"。不实现通用的deflate/inflate, // 不依赖任何第三方库 // ==================================================================== #pragma pack(push, 1) struct EOCDRecord { uint32_t sig; // 0x06054b50 uint16_t diskNum; uint16_t cdStartDisk; uint16_t numEntriesDisk; uint16_t numEntriesTotal; uint32_t cdSize; uint32_t cdOffset; uint16_t commentLen; }; #pragma pack(pop) struct ZipEntryInfo { std::string name; uint16_t versionMadeBy = 20; uint16_t versionNeeded = 20; uint16_t flags = 0; uint16_t method = 0; uint16_t modTime = 0; uint16_t modDate = 0; uint32_t crc32 = 0; uint32_t compSize = 0; uint32_t uncompSize = 0; uint32_t localHeaderOffset = 0; // 在原zip文件里的偏移(仅passthrough条目用得到) }; static bool ReadU16(const std::vector<unsigned char>& b, size_t off, uint16_t& v) { if (off + 2 > b.size()) return false; v = (uint16_t)(b[off] | (b[off + 1] << 8)); return true; } static bool ReadU32(const std::vector<unsigned char>& b, size_t off, uint32_t& v) { if (off + 4 > b.size()) return false; v = (uint32_t)b[off] | ((uint32_t)b[off + 1] << 8) | ((uint32_t)b[off + 2] << 16) | ((uint32_t)b[off + 3] << 24); return true; } // 从文件末尾往前找EOCD签名(0x06054b50)。我们自己的模版文件没有zip // comment,理论上EOCD正好在文件最后22字节,但保险起见往前多扫一点 static bool FindEOCD(const std::vector<unsigned char>& b, size_t& eocdOff) { if (b.size() < 22) return false; size_t searchStart = (b.size() > 66000) ? b.size() - 66000 : 0; for (size_t i = b.size() - 22; ; i--) { uint32_t sig; if (ReadU32(b, i, sig) && sig == 0x06054b50u) { eocdOff = i; return true; } if (i == searchStart || i == 0) break; } return false; } static bool ReadZipEntries(const std::vector<unsigned char>& b, std::vector<ZipEntryInfo>& entries, std::string& err) { size_t eocdOff; if(!FindEOCD(b, eocdOff)) { err = "not a valid zip (EOCD not found)"; return false; } uint16_t numEntries; uint32_t cdOffset, cdSize; if (!ReadU16(b, eocdOff + 10, numEntries) || !ReadU32(b, eocdOff + 12, cdSize) || !ReadU32(b, eocdOff + 16, cdOffset)) { err = "bad EOCD"; return false; } size_t p = cdOffset; for (uint16_t i = 0; i < numEntries; i++) { uint32_t sig; if (!ReadU32(b, p, sig) || sig != 0x02014b50u) { err = "bad central dir entry"; return false; } ZipEntryInfo e; uint16_t fnameLen, extraLen, commentLen; ReadU16(b, p + 4, e.versionMadeBy); ReadU16(b, p + 6, e.versionNeeded); ReadU16(b, p + 8, e.flags); ReadU16(b, p + 10, e.method); ReadU16(b, p + 12, e.modTime); ReadU16(b, p + 14, e.modDate); ReadU32(b, p + 16, e.crc32); ReadU32(b, p + 20, e.compSize); ReadU32(b, p + 24, e.uncompSize); ReadU16(b, p + 28, fnameLen); ReadU16(b, p + 30, extraLen); ReadU16(b, p + 32, commentLen); ReadU32(b, p + 42, e.localHeaderOffset); size_t nameOff = p + 46; if (nameOff + fnameLen > b.size()) { err = "bad filename range"; return false; } e.name.assign((const char*)&b[nameOff], fnameLen); entries.push_back(e); p = nameOff + fnameLen + extraLen + commentLen; } return true; } // 定位某条目在原文件里"本地文件头起点"到"数据结尾"的完整字节区间 // (本地文件头+文件名+extra+数据),这一整段原样拷贝即可,不用关心 // 里面是压缩还是不压缩的 static bool GetLocalEntrySpan(const std::vector<unsigned char>& b, const ZipEntryInfo& e, size_t& start, size_t& len, std::string& err) { start = e.localHeaderOffset; uint32_t sig; if (!ReadU32(b, start, sig) || sig != 0x04034b50u) { err = "bad local header for " + e.name; return false; } uint16_t fnameLen, extraLen; if (!ReadU16(b, start + 26,fnameLen) || !ReadU16(b, start + 28, extraLen)) { err = "bad local header fields"; return false; } len = 30 + fnameLen + extraLen + e.compSize; if (start + len > b.size()) { err = "local entry span exceeds file size for " + e.name; return false; } return true; } static bool ReadWholeFile(const std::string& path, std::vector<unsigned char>& out, std::string& err) { std::ifstream f; OpenIn(f, path); if (!f) { err = "cannot open file: " + path; return false; } f.seekg(0, std::ios::end); std::streamoff sz = f.tellg(); f.seekg(0, std::ios::beg); if (sz < 0) { err = "cannot stat file: " + path; return false; } out.resize((size_t)sz); if (sz > 0) f.read((char*)out.data(), sz); return true; } // 写一个zip本地文件头(LFH)到输出流,返回是否成功。STORED方式: // method=0,compSize==uncompSize static void WriteU16(std::ofstream& f, uint16_t v) { f.write((const char*)&v, 2); } static void WriteU32(std::ofstream& f, uint32_t v) { f.write((const char*)&v, 4); } static void WriteLocalHeaderAndData(std::ofstream& out, const std::string& name, uint16_t method, uint16_t modTime, uint16_t modDate, uint32_t crc, uint32_t compSize, uint32_t uncompSize, const std::string& data) { WriteU32(out, 0x04034b50u); WriteU16(out, 20); // version needed WriteU16(out, 0); // flags WriteU16(out, method); WriteU16(out, modTime); WriteU16(out, modDate); WriteU32(out, crc); WriteU32(out, compSize); WriteU32(out, uncompSize); WriteU16(out, (uint16_t)name.size()); WriteU16(out, 0); // extra len out.write(name.data(), (std::streamsize)name.size()); out.write(data.data(), (std::streamsize)data.size()); } // 用模版zip"克隆"出最终.xlsx:除xl/worksheets/sheet1.xml外的所有条目 // 原始字节整体拷贝,sheet1.xml用新内容以STORED方式写入 static bool CloneZipReplacingSheet(const std::string& tplPath, const std::string& outPath, const std::string& newSheetXml, std::string& err) { std::vector<unsigned char> tpl; if (!ReadWholeFile(tplPath, tpl, err)) return false; std::vector<ZipEntryInfo> entries; if (!ReadZipEntries(tpl, entries, err)) return false; // 找模版里sheet1.xml原有的mod_time/mod_date,新条目沿用这个值 // (zip本地时间字段只是元数据,不影响正确性,沿用一个合法值即可) uint16_t sheetModTime = 0, sheetModDate = 0; bool foundSheet = false; for (auto& e : entries) { if (e.name == "xl/worksheets/sheet1.xml") { sheetModTime = e.modTime; sheetModDate = e.modDate; foundSheet = true; break; } } if (!foundSheet) { err = "template missing xl/worksheets/sheet1.xml"; return false; } std::ofstream out; OpenOut(out, outPath, std::ios::trunc); if (!out) { err = "cannot create output file: " + outPath; return false; } struct WrittenEntry { std::string name; uint16_t method, modTime, modDate; uint32_t crc, compSize, uncompSize, offset; }; std::vector<WrittenEntry> written; written.reserve(entries.size()); for (auto& e : entries) { if (e.name == "xl/worksheets/sheet1.xml") continue; // 最后单独处理 size_t spanStart, spanLen; if (!GetLocalEntrySpan(tpl, e, spanStart, spanLen, err)) return false; uint32_t newOffset = (uint32_t)out.tellp(); out.write((const char*)&tpl[spanStart], (std::streamsize)spanLen); written.push_back({ e.name, e.method, e.modTime, e.modDate, e.crc32, e.compSize, e.uncompSize, newOffset }); } // sheet1.xml:全新内容,STORED不压缩 { uint32_t crc = Crc32(newSheetXml); uint32_t sz = (uint32_t)newSheetXml.size(); uint32_t newOffset = (uint32_t)out.tellp(); WriteLocalHeaderAndData(out, "xl/worksheets/sheet1.xml", 0, sheetModTime, sheetModDate, crc, sz, sz, newSheetXml); written.push_back({ "xl/worksheets/sheet1.xml", 0, sheetModTime, sheetModDate, crc, sz, sz, newOffset }); } uint32_t cdStart = (uint32_t)out.tellp(); for (auto& w : written) { WriteU32(out, 0x02014b50u); WriteU16(out, 20); // version made by WriteU16(out, 20); // version needed WriteU16(out, 0); // flags WriteU16(out, w.method); WriteU16(out, w.modTime); WriteU16(out, w.modDate); WriteU32(out, w.crc); WriteU32(out, w.compSize); WriteU32(out, w.uncompSize); WriteU16(out, (uint16_t)w.name.size()); WriteU16(out, 0); // extra len WriteU16(out, 0); // comment len WriteU16(out, 0); // disk number start WriteU16(out, 0); // internal attrs WriteU32(out, 0); // external attrs WriteU32(out, w.offset); out.write(w.name.data(), (std::streamsize)w.name.size()); } uint32_t cdSize = (uint32_t)((uint32_t)out.tellp() - cdStart); WriteU32(out, 0x06054b50u); WriteU16(out, 0); WriteU16(out, 0); WriteU16(out, (uint16_t)written.size()); WriteU16(out, (uint16_t)written.size()); WriteU32(out, cdSize); WriteU32(out, cdStart); WriteU16(out, 0); out.close(); return true; } // ==================================================================== // 6. sheet1.xml内容拼装。行1-3/行9(标题+列名)是模版固定内容,原样 // 硬编码;行4-8(22个筛选值)、行10..(9+N)(明细)、第(10+N)行(合计) // 按实际数据动态生成。所有单元格坐标/合并范围跟模版 // xl/worksheets/sheet1.xml反查结果完全一致(见文件开头注释的坐标表) // ==================================================================== // 40列里哪些是数字列(1-based列号):A=1(序号) AH=34(订单数量) // AI=35(已出货数量) AJ=36(待出货数量) AN=40(生产数量(Y)),其余36列 // 都是文本列。跟模版row10样例行的样式(数字列s=6,文本列s=7)一致 static bool IsNumericCol(int col1based) { return col1based== 1 || col1based == 34 || col1based == 35 || col1based == 36 || col1based == 40; } // V10 流式重构:整张sheet1.xml拆成三段--表头(row1-9)/逐行明细/合计行+收尾。 // FINAL阶段从.rows.tmp逐行读、逐段直接写进输出文件,任何时刻内存里只有"一行" // 的XML,占用与总行数无关(20万、200万行都不会爆32位内存)。这三个函数就是 // 原BuildSheetXml按边界原样拆开的,单元格/样式/合并逻辑一字未改。 // BuildSheetHead:表头(row1-9)。<dimension>要用到总行数N(footerRow=10+N) static std::string BuildSheetHead(const std::vector<std::string>& h, int N) { auto H = [&](size_t i) -> std::string { return (i < h.size()) ? h[i] : std::string(); }; int footerRow = 10 + N; std::string xml; xml.reserve(24000); xml += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n"; xml += "<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " "xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" " "xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" " "xmlns:x14ac=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac\" " "mc:Ignorable=\"x14ac\">"; xml += "<dimension ref=\"A1:AN" + std::to_string(footerRow) + "\"/>"; xml += "<sheetViews><sheetView tabSelected=\"1\" view=\"pageBreakPreview\" zoomScaleNormal=\"100\" " "zoomScaleSheetLayoutView=\"100\" workbookViewId=\"0\"><pane ySplit=\"9\" topLeftCell=\"A10\" " "activePane=\"bottomLeft\" state=\"frozenSplit\"/><selection pane=\"bottomLeft\" activeCell=\"A10\" " "sqref=\"A10\"/></sheetView></sheetViews>"; xml += "<sheetFormatPr defaultRowHeight=\"15.6\" x14ac:dyDescent=\"0.25\"/>"; xml += "<cols>" "<col min=\"1\" max=\"1\" width=\"3\" customWidth=\"1\"/>" "<col min=\"2\" max=\"2\" width=\"5.09765625\" customWidth=\"1\"/>" "<col min=\"3\" max=\"3\" width=\"3.5\" customWidth=\"1\"/>" "<col min=\"4\" max=\"4\" width=\"4\" customWidth=\"1\"/>" "<col min=\"5\" max=\"5\" width=\"3\" customWidth=\"1\"/>" "<col min=\"6\" max=\"6\" width=\"3.09765625\" customWidth=\"1\"/>" "<col min=\"7\" max=\"7\" width=\"3\" customWidth=\"1\"/>" "<col min=\"8\" max=\"8\" width=\"3.59765625\" customWidth=\"1\"/>" "<col min=\"9\" max=\"9\" width=\"4.09765625\" customWidth=\"1\"/>" "<col min=\"10\" max=\"10\" width=\"4\" customWidth=\"1\"/>" "<col min=\"11\" max=\"11\" width=\"2.5\" customWidth=\"1\"/>" "<col min=\"12\" max=\"12\" width=\"3.69921875\" customWidth=\"1\"/>" "<col min=\"13\" max=\"13\" width=\"3\" customWidth=\"1\"/>" "<col min=\"14\" max=\"14\" width=\"4\" customWidth=\"1\"/>" "<col min=\"15\" max=\"15\" width=\"3.19921875\" customWidth=\"1\"/>" "<col min=\"16\" max=\"16\" width=\"3\" customWidth=\"1\"/>" "<col min=\"17\" max=\"17\" width=\"3.5\" customWidth=\"1\"/>" "<col min=\"18\" max=\"18\" width=\"4.8984375\" customWidth=\"1\"/>" "<col min=\"19\" max=\"19\" width=\"2.59765625\" customWidth=\"1\"/>" "<col min=\"20\" max=\"20\" width=\"5\" customWidth=\"1\"/>" "<col min=\"21\" max=\"21\" width=\"4\" customWidth=\"1\"/>" "<col min=\"22\" max=\"22\" width=\"3.3984375\" customWidth=\"1\"/>" "<col min=\"23\" max=\"23\" width=\"4.5\" customWidth=\"1\"/>" "<col min=\"24\" max=\"24\" width=\"2.5\" customWidth=\"1\"/>" "<col min=\"25\" max=\"25\" width=\"10.796875\" customWidth=\"1\"/>" "<col min=\"26\" max=\"26\" width=\"2.59765625\" customWidth=\"1\"/>" "<col min=\"27\" max=\"27\" width=\"8.3984375\" customWidth=\"1\"/>" "<col min=\"28\" max=\"28\" width=\"4.09765625\" customWidth=\"1\"/>" "<col min=\"29\" max=\"29\" width=\"3.59765625\" customWidth=\"1\"/>" "<col min=\"30\" max=\"33\" width=\"2.8984375\" customWidth=\"1\"/>" "<col min=\"34\" max=\"36\" width=\"4.8984375\" customWidth=\"1\"/>" "<col min=\"37\" max=\"37\" width=\"3\" customWidth=\"1\"/>" "<col min=\"38\" max=\"38\" width=\"5.69921875\" customWidth=\"1\"/>" "<col min=\"39\" max=\"39\" width=\"5.5\" customWidth=\"1\"/>" "<col min=\"40\" max=\"40\" width=\"5\" customWidth=\"1\"/>" "</cols>"; xml += "<sheetData>"; // ---- row1-3: 公司名/报表标题,模版固定内容 ---- xml += "<row r=\"1\" spans=\"1:40\" ht=\"19.2\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A1", "11", "成 隆 实 业 有 限 公 司/C\xc3\x94NG TY TNHH SUCCESS LOYAL"); for (int c = 2; c <= 40; c++) xml += "<c r=\"" + ColLetter(c) + "1\" s=\"12\"/>"; xml += "</row>"; xml += "<row r=\"2\" spans=\"1:40\" ht=\"19.2\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A2", "11", "\xe8\xb6\x8a\xe5\x8d\x97\xe6\x88\x90\xe9\x9a\x86\xe7\xba\xba\xe7\xbb\x87\xe8\xb4\xa3\xe4\xbb\xbb\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8/C\xc3\x94NG TY TNHH D\xe1\xbb\x86T SUCCESS LOYAL VI\xe1\xbb\x86T NAM"); for (int c = 2; c <= 40; c++) xml += "<c r=\"" + ColLetter(c) + "2\" s=\"12\"/>"; xml += "</row>"; xml += "<row r=\"3\" spans=\"1:40\" ht=\"19.2\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A3", "11", "\xe5\xae\xa2\xe6\x88\xb7\xe8\xae\xa2\xe5\x8d\x95\xe6\x98\x8e\xe7\xbb\x86\xe8\xa1\xa8 / Chi ti\xe1\xba\xbft \xc4\x91\xc6\xa1n h\xc3\xa0ng c\xe1\xbb\xa7" "a kh\xc3\xa1" "ch h\xc3\xa0ng"); for (int c = 2; c <= 40; c++) xml += "<c r=\"" + ColLetter(c) + "3\" s=\"12\"/>"; xml += "</row>"; // ---- row4: 开始日期(D4,h1,数字日期) 结束日期(L4,h2,数字日期) // 客户(R4,h3) 内部订号(W4,h4) 内部物料编号(AB4,h5) 客户物料代码(AI4,h6) ---- { std::string d1 = DateStrToSerialOrEmpty(H(0)); std::string d2 = DateStrToSerialOrEmpty(H(1)); xml += "<row r=\"4\" spans=\"1:40\" ht=\"14.4\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A4", "8", "\xe5\xbc\x80\xe5\xa7\x8b\xe6\x97\xa5\xe6\x9c\x9f\xef\xbc\x9a"); xml += "<c r=\"B4\" s=\"8\"/><c r=\"C4\" s=\"8\"/>"; xml += d1.empty() ? "<c r=\"D4\" s=\"13\"/>" : ("<c r=\"D4\" s=\"13\"><v>" + d1 + "</v></c>"); xml += "<c r=\"E4\" s=\"14\"/><c r=\"F4\" s=\"14\"/><c r=\"G4\" s=\"14\"/>"; xml += CellInlineStr("H4", "8", "\xe7\xbb\x93\xe6\x9d\x9f\xe6\x97\xa5\xe6\x9c\x9f\xef\xbc\x9a"); xml += "<c r=\"I4\" s=\"8\"/><c r=\"J4\" s=\"8\"/><c r=\"K4\" s=\"8\"/>"; xml += d2.empty() ? "<c r=\"L4\" s=\"13\"/>" : ("<c r=\"L4\" s=\"13\"><v>" + d2 + "</v></c>"); xml += "<c r=\"M4\" s=\"14\"/><c r=\"N4\" s=\"14\"/><c r=\"O4\" s=\"14\"/>"; xml += CellInlineStr("P4", "8", "\xe5\xae\xa2\xe6\x88\xb7\xef\xbc\x9a"); xml += "<c r=\"Q4\" s=\"8\"/>"; xml += CellInlineStr("R4", "14", H(2)); xml += "<c r=\"S4\" s=\"14\"/>"; xml += CellInlineStr("T4", "8", "\xe5\x86\x85\xe9\x83\xa8\xe8\xae\xa2\xe5\x8d\x95\xe5\x8f\xb7\xef\xbc\x9a"); xml += "<c r=\"U4\" s=\"8\"/><c r=\"V4\" s=\"8\"/>"; xml += CellInlineStr("W4", "14", H(3)); xml += "<c r=\"X4\" s=\"14\"/><c r=\"Y4\" s=\"14\"/>"; xml += CellInlineStr("Z4", "8", "\xe5\x86\x85\xe9\x83\xa8\xe7\x89\xa9\xe6\x96\x99\xe7\xbc\x96\xe5\x8f\xb7\xef\xbc\x9a"); xml += "<c r=\"AA4\" s=\"8\"/>"; xml += CellInlineStr("AB4", "14", H(4)); xml += "<c r=\"AC4\" s=\"14\"/><c r=\"AD4\" s=\"14\"/>"; xml += CellInlineStr("AE4", "8", "\xe5\xae\xa2\xe6\x88\xb7\xe7\x89\xa9\xe6\x96\x99\xe4\xbb\xa3\xe7\xa0\x81\xef\xbc\x9a"); xml += "<c r=\"AF4\" s=\"8\"/><c r=\"AG4\" s=\"8\"/><c r=\"AH4\" s=\"8\"/>"; xml += CellInlineStr("AI4", "14", H(5)); xml += "<c r=\"AJ4\" s=\"14\"/><c r=\"AK4\" s=\"14\"/><c r=\"AL4\" s=\"14\"/><c r=\"AM4\" s=\"14\"/><c r=\"AN4\" s=\"14\"/>"; xml += "</row>"; } // ---- row5: 工单号(C5,h7) 预告工单号(L5,h8,一般空) 客户采购单号(S5,h9) 客户指令(Z5,h10) ---- xml += "<row r=\"5\" spans=\"1:40\" ht=\"14.4\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A5", "8", "\xe5\xb7\xa5\xe5\x8d\x95\xe5\x8f\xb7\xef\xbc\x9a"); xml += "<c r=\"B5\" s=\"8\"/>"; xml += CellInlineStr("C5", "14", H(6)); xml += "<c r=\"D5\" s=\"14\"/><c r=\"E5\" s=\"14\"/><c r=\"F5\" s=\"14\"/><c r=\"G5\" s=\"14\"/>"; xml += CellInlineStr("H5", "8", "\xe9\xa2\x84\xe5\x91\x8a\xe5\xb7\xa5\xe5\x8d\x95\xe5\x8f\xb7 \xef\xbc\x9a"); xml += "<c r=\"I5\" s=\"8\"/><c r=\"J5\" s=\"8\"/><c r=\"K5\" s=\"8\"/>"; xml += CellInlineStr("L5", "17", H(7)); xml += "<c r=\"M5\" s=\"17\"/><c r=\"N5\" s=\"17\"/><c r=\"O5\" s=\"17\"/>"; xml += CellInlineStr("P5", "8", "\xe5\xae\xa2\xe6\x88\xb7\xe9\x87\x87\xe8\xb4\xad\xe5\x8d\x95\xe5\x8f\xb7\xef\xbc\x9a"); xml += "<c r=\"Q5\" s=\"8\"/><c r=\"R5\" s=\"8\"/>"; xml += CellInlineStr("S5", "14", H(8)); xml += "<c r=\"T5\" s=\"14\"/><c r=\"U5\" s=\"14\"/><c r=\"V5\" s=\"14\"/><c r=\"W5\" s=\"14\"/><c r=\"X5\" s=\"14\"/>"; xml += CellInlineStr("Y5", "4", "\xe5\xae\xa2\xe6\x88\xb7\xe6\x8c\x87\xe4\xbb\xa4\xef\xbc\x9a"); xml += CellInlineStr("Z5", "14", H(9)); for (int c = 27; c <= 40; c++) xml += "<c r=\"" + ColLetter(c) + "5\" s=\"14\"/>"; xml += "</row>"; // ---- row6: 客户型体(C6,h11) 通用色号(S6,h12) 内部色号(AI6,h13) ---- xml += "<row r=\"6\" spans=\"1:40\" ht=\"14.4\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A6", "8", "\xe5\xae\xa2\xe6\x88\xb7\xe5\x9e\x8b\xe4\xbd\x93\xef\xbc\x9a"); xml += "<c r=\"B6\" s=\"8\"/>"; xml += CellInlineStr("C6", "14", H(10)); for (int c = 4; c <= 15; c++) xml += "<c r=\"" +ColLetter(c) + "6\" s=\"14\"/>"; xml+= CellInlineStr("P6", "8", "\xe9\x80\x9a\xe7\x94\xa8\xe8\x89\xb2\xe5\x8f\xb7\xef\xbc\x9a"); xml += "<c r=\"Q6\" s=\"8\"/><c r=\"R6\" s=\"8\"/>"; xml += CellInlineStr("S6", "14", H(11)); for (int c = 20; c <= 30; c++) xml += "<c r=\"" + ColLetter(c) + "6\" s=\"14\"/>"; xml += CellInlineStr("AE6", "8", "\xe5\x86\x85\xe9\x83\xa8\xe8\x89\xb2\xe5\x8f\xb7\xef\xbc\x9a"); xml += "<c r=\"AF6\" s=\"8\"/><c r=\"AG6\" s=\"8\"/><c r=\"AH6\" s=\"8\"/>"; xml += CellInlineStr("AI6", "14", H(12)); xml += "<c r=\"AJ6\" s=\"14\"/><c r=\"AK6\" s=\"14\"/><c r=\"AL6\" s=\"14\"/><c r=\"AM6\" s=\"14\"/><c r=\"AN6\" s=\"14\"/>"; xml += "</row>"; // ---- row7: 是否打确认样(E7,h14) 是否防虹吸(K7,h15) 订购胶片(P7,h16) // 织带印刷(V7,h17) 客户订单备注(AA7,h18) 明细备注(AI7,h19) ---- xml += "<row r=\"7\" spans=\"1:40\" ht=\"14.4\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A7", "8", "\xe6\x98\xaf\xe5\x90\xa6\xe6\x89\x93\xe7\xa1\xae\xe8\xae\xa4\xe6\xa0\xb7\xef\xbc\x9a"); xml += "<c r=\"B7\" s=\"8\"/><c r=\"C7\" s=\"8\"/><c r=\"D7\" s=\"8\"/>"; xml += CellInlineStr("E7", "14", H(13)); xml += "<c r=\"F7\" s=\"14\"/><c r=\"G7\" s=\"14\"/>"; xml += CellInlineStr("H7", "8", "\xe6\x98\xaf\xe5\x90\xa6\xe9\x98\xb2\xe8\x99\xb9\xe5\x90\xb8\xef\xbc\x9a"); xml += "<c r=\"I7\" s=\"8\"/><c r=\"J7\" s=\"8\"/>"; xml += CellInlineStr("K7", "14", H(14)); xml += "<c r=\"L7\" s=\"14\"/>"; xml += CellInlineStr("M7", "8", "\xe8\x83\xb6\xe7\x89\x87\xe8\xae\xa2\xe8\xb4\xad\xef\xbc\x9a"); xml += "<c r=\"N7\" s=\"8\"/><c r=\"O7\" s=\"8\"/>"; xml += CellInlineStr("P7", "14", H(15)); xml += "<c r=\"Q7\" s=\"14\"/><c r=\"R7\" s=\"14\"/>"; xml += CellInlineStr("S7", "8", "\xe7\xbb\x87\xe5\xb8\xa6\xe5\x8d\xb0\xe5\x88\xb7\xef\xbc\x9a"); xml += "<c r=\"T7\" s=\"8\"/><c r=\"U7\" s=\"8\"/>"; xml += CellInlineStr("V7", "14", H(16)); xml += "<c r=\"W7\" s=\"14\"/><c r=\"X7\" s=\"14\"/>"; xml += CellInlineStr("Y7", "8", "\xe5\xae\xa2\xe6\x88\xb7\xe8\xae\xa2\xe5\x8d\x95\xe5\xa4\x87\xe6\xb3\xa8\xef\xbc\x9a"); xml += "<c r=\"Z7\" s=\"8\"/>"; xml += CellInlineStr("AA7", "14", H(17)); xml += "<c r=\"AB7\" s=\"14\"/><c r=\"AC7\" s=\"14\"/><c r=\"AD7\" s=\"14\"/>"; xml += CellInlineStr("AE7", "8", "\xe6\x98\x8e\xe7\xbb\x86\xe5\xa4\x87\xe6\xb3\xa8\xef\xbc\x9a"); xml += "<c r=\"AF7\" s=\"8\"/><c r=\"AG7\" s=\"8\"/><c r=\"AH7\" s=\"8\"/>"; xml += CellInlineStr("AI7", "14", H(18)); xml += "<c r=\"AJ7\" s=\"14\"/><c r=\"AK7\" s=\"14\"/><c r=\"AL7\" s=\"14\"/><c r=\"AM7\" s=\"14\"/><c r=\"AN7\" s=\"14\"/>"; xml += "</row>"; // ---- row8: 品名描述(D8,h20) 订单单位(AI8,h21) 出货状态(AM8,h22) ---- xml += "<row r=\"8\" spans=\"1:40\" ht=\"14.4\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A8", "8", "\xe5\x93\x81\xe5\x90\x8d\xe6\x8f\x8f\xe8\xbf\xb0\xef\xbc\x9a"); xml += "<c r=\"B8\" s=\"8\"/><c r=\"C8\" s=\"8\"/>"; xml += CellInlineStr("D8", "20", H(19)); for (int c = 5; c <= 29; c++) xml += "<c r=\"" + ColLetter(c) + "8\" s=\"22\"/>"; xml += "<c r=\"AD8\" s=\"21\"/>"; xml += CellInlineStr("AE8", "8", "\xe8\xae\xa2\xe5\x8d\x95\xe5\x8d\x95\xe4\xbd\x8d\xef\xbc\x9a"); xml += "<c r=\"AF8\" s=\"8\"/><c r=\"AG8\" s=\"8\"/><c r=\"AH8\" s=\"8\"/>"; xml += CellInlineStr("AI8", "20", H(20)); xml += "<c r=\"AJ8\" s=\"21\"/>"; xml += CellInlineStr("AK8", "18", "\xe5\x87\xba\xe8\xb4\xa7\xe7\x8a\xb6\xe6\x80\x81\xef\xbc\x9a"); xml += "<c r=\"AL8\" s=\"19\"/>"; xml += CellInlineStr("AM8", "20", H(21)); xml += "<c r=\"AN8\" s=\"21\"/>"; xml += "</row>"; // ---- row9: 列标题,固定不变 ---- xml += "<row r=\"9\" spans=\"1:40\" ht=\"72\" x14ac:dyDescent=\"0.25\">"; { static const char* kHeaders[40] = { "\xe5\xba\x8f\xe5\x8f\xb7", "\xe8\xae\xa2\xe5\x8d\x95\xe5\x8d\x95\xe6\x8d\xae\xe7\xbc\x96\xe5\x8f\xb7", "\xe5\x8d\x95\xe6\x8d\xae\xe6\x97\xa5\xe6\x9c\x9f", "\xe5\x88\xb6\xe5\x8d\x95\xe4\xba\xba", "\xe5\x8d\x95\xe6\x8d\xae\xe7\x8a\xb6\xe6\x80\x81", "\xe5\xae\xa1\xe6\xa0\xb8\xe4\xba\xba", "\xe5\xae\xa2\xe6\x88\xb7\xe7\xbc\x96\xe5\x8f\xb7", "\xe4\xb8\x9a\xe5\x8a\xa1\xe5\x91\x98", "\xe5\x86\x85\xe9\x83\xa8\xe8\xae\xa2\xe5\x8f\xb7", "\xe8\xae\xa2\xe5\x8d\x95\xe7\xb1\xbb\xe5\x9e\x8b", "\xe7\x94\x9f\xe4\xba\xa7\xe9\x83\xa8\xe9\x97\xa8", "\xe6\x88\x90\xe5\x93\x81\xe9\xa1\xb9\xe6\xac\xa1", "\xe5\xb7\xa5\xe5\x8d\x95\xe5\x8f\xb7", "\xe9\xa2\x84\xe5\x91\x8a\xe5\xb7\xa5\xe5\x8d\x95\xe5\x8f\xb7", "\xe4\xb8\x8b\xe5\x8d\x95\xe6\x97\xa5\xe6\x9c\x9f", "\xe4\xba\xa4\xe4\xbb\x98\xe6\x97\xa5\xe6\x9c\x9f", "\xe5\xae\xa2\xe6\x88\xb7\xe9\x87\x87\xe8\xb4\xad\xe5\x8d\x95\xe5\x8f\xb7", "\xe5\xae\xa2\xe6\x88\xb7\xe5\x9e\x8b\xe4\xbd\x93", "\xe9\xa1\xb9\xe6\xac\xa1", "\xe5\xae\xa2\xe6\x88\xb7\xe6\x8c\x87\xe4\xbb\xa4", "\xe4\xbc\x81\xe5\x88\x92\xe5\x8d\x95\xe5\x8f\xb7", "\xe4\xbc\x81\xe4\xb8\x9a\xe5\x8d\x95\xe5\x8f\xb7", "\xe5\xae\xa2\xe6\x88\xb7\xe7\x89\xa9\xe6\x96\x99\xe7\xbc\x96\xe5\x8f\xb7", "\xe5\x86\x85\xe9\x83\xa8\xe6\x96\x99\xe5\x8f\xb7", "\xe5\x93\x81\xe5\x90\x8d\xe6\x8f\x8f\xe8\xbf\xb0", "\xe8\xa7\x84\xe6\xa0\xbc", "\xe9\x80\x9a\xe7\x94\xa8\xe8\x89\xb2\xe5\x8f\xb7", "\xe5\x86\x85\xe9\x83\xa8\xe8\x89\xb2\xe5\x8f\xb7", "\xe9\x95\xbf\xe5\xba\xa6", "\xe6\x98\xaf\xe5\x90\xa6\xe6\x89\x93\xe7\xa1\xae\xe8\xae\xa4\xe6\xa0\xb7", "\xe6\x98\xaf\xe5\x90\xa6\xe9\x98\xb2\xe8\x99\xb9\xe5\x90\xb8", "\xe8\xae\xa2\xe8\xb4\xad\xe8\x83\xb6\xe7\x89\x87", "\xe7\xbb\x87\xe5\xb8\xa6\xe5\x8d\xb0\xe5\x88\xb7", "\xe8\xae\xa2\xe5\x8d\x95\xe6\x95\xb0\xe9\x87\x8f", "\xe5\xb7\xb2\xe5\x87\xba\xe8\xb4\xa7\xe6\x95\xb0\xe9\x87\x8f", "\xe5\xbe\x85\xe5\x87\xba\xe8\xb4\xa7\xe6\x95\xb0\xe9\x87\x8f", "\xe8\xae\xa2\xe5\x8d\x95\xe5\x8d\x95\xe4\xbd\x8d", "\xe5\xae\xa2\xe6\x88\xb7\xe8\xae\xa2\xe5\x8d\x95\xe5\xa4\x87\xe6\xb3\xa8", "\xe5\xa4\x87\xe6\xb3\xa8", "\xe7\x94\x9f\xe4\xba\xa7\xe6\x95\xb0\xe9\x87\x8f(Y)" }; for (int c = 1; c <= 40; c++) { std::string style = (c == 39) ? "15" : "5"; // AM9(备注,列39)模版样式是15,其余是5 xml += CellInlineStr(ColLetter(c) + "9", style, kHeaders[c - 1]); } } xml += "</row>"; return xml; } // AppendDataRow:拼一行明细(第r行,40列;数字列s=6、文本列s=7),追加到xml。 // 就是原BuildSheetXml里数据循环的循环体,被流式一行一行地调用 static void AppendDataRow(std::string& xml, const std::vector<std::string>& fields, int r) { xml += "<row r=\"" + std::to_string(r) + "\" spans=\"1:40\" ht=\"144\" x14ac:dyDescent=\"0.25\">"; for (int c = 1; c <= 40; c++) { std::string ref = ColLetter(c) + std::to_string(r); std::string val = (c - 1 < (int)fields.size()) ? fields[c - 1] : std::string(); if (IsNumericCol(c)) { xml += CellNum(ref, "6", val); } else { xml += CellInlineStr(ref, "7", val); } } xml += "</row>"; } // BuildSheetTail:合计行(第10+N行)+ </sheetData> + autoFilter + mergeCells + 收尾。 // 要用到总行数N static std::string BuildSheetTail(const std::vector<std::string>& totals, int N) { auto T = [&](size_t i) -> std::string { return (i < totals.size()) ? totals[i] : std::string(); }; int footerRow = 10 + N; std::string xml; xml.reserve(6000); // ---- 第(10+N)行: 查询人(E:J合并,值=T(0)) 查询日期(P:U合并,值=T(1)) // 合计:订单数量(AH,T(2)) 已出货数量(AI,T(3)) 待出货数量(AJ,T(4)) 生产数量Y(AN,T(5)) ---- { std::string fr = std::to_string(footerRow); xml += "<row r=\"" + fr + "\" spans=\"1:40\" ht=\"25.2\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A" + fr, "8", "\xe6\x9f\xa5\xe8\xaf\xa2\xe4\xba\xba \nNg\xc6\xb0\xe1\xbb\x9di truy v\xe1\xba\xa5n\xef\xbc\x9a"); xml += "<c r=\"B" + fr + "\"s=\"8\"/><c r=\"C"+fr + "\" s=\"8\"/><c r=\"D" + fr + "\" s=\"8\"/>"; xml += CellInlineStr("E" + fr, "9", T(0)); xml += "<c r=\"F" + fr + "\" s=\"9\"/><c r=\"G" + fr + "\" s=\"9\"/><c r=\"H" + fr + "\" s=\"9\"/><c r=\"I" + fr + "\" s=\"9\"/><c r=\"J" + fr + "\" s=\"9\"/>"; xml += CellInlineStr("K" + fr, "8", "\xe6\x9f\xa5\xe8\xaf\xa2\xe6\x97\xa5\xe6\x9c\x9f \nNg\xc3\xa0y truy v\xe1\xba\xa5n\xef\xbc\x9a"); xml += "<c r=\"L" + fr + "\" s=\"8\"/><c r=\"M" + fr + "\" s=\"8\"/><c r=\"N" + fr + "\" s=\"8\"/><c r=\"O" + fr + "\" s=\"8\"/>"; xml += CellInlineStr("P" + fr, "8", T(1)); xml += "<c r=\"Q" + fr + "\" s=\"8\"/><c r=\"R" + fr + "\" s=\"8\"/><c r=\"S" + fr + "\" s=\"8\"/><c r=\"T" + fr + "\" s=\"8\"/><c r=\"U" + fr + "\" s=\"8\"/>"; xml += "<c r=\"V" + fr + "\" s=\"9\"/><c r=\"W" + fr + "\" s=\"9\"/><c r=\"X" + fr + "\" s=\"9\"/><c r=\"Y" + fr + "\" s=\"9\"/><c r=\"Z" + fr + "\" s=\"9\"/><c r=\"AA" + fr + "\" s=\"9\"/><c r=\"AB" + fr + "\" s=\"9\"/><c r=\"AC" + fr + "\" s=\"9\"/>"; xml += CellInlineStr("AD" + fr, "10", "\xe5\x90\x88\xe8\xae\xa1\xef\xbc\x9a\nT\xe1\xbb\x95ng c\xe1\xbb\x99ng"); xml += "<c r=\"AE" + fr + "\" s=\"10\"/><c r=\"AF" + fr + "\" s=\"10\"/><c r=\"AG" + fr + "\" s=\"10\"/>"; xml += CellNum("AH" + fr, "16", T(2)); xml += CellNum("AI" + fr, "16", T(3)); xml += CellNum("AJ" + fr, "16", T(4)); xml += "<c r=\"AK" + fr + "\" s=\"16\"/><c r=\"AL" + fr + "\" s=\"16\"/><c r=\"AM" + fr + "\" s=\"16\"/>"; xml += CellNum("AN" + fr, "16", T(5)); xml += "</row>"; } xml += "</sheetData>"; xml += "<autoFilter ref=\"A9:AN" + std::to_string(footerRow - 1) + "\"/>"; // 合并单元格:1-8行标题区(46处,行号固定不变) + 第(10+N)行(合计行,6处) xml += "<mergeCells count=\"52\">"; xml += "<mergeCell ref=\"A1:AN1\"/><mergeCell ref=\"A2:AN2\"/><mergeCell ref=\"A3:AN3\"/>"; xml += "<mergeCell ref=\"A4:C4\"/><mergeCell ref=\"D4:G4\"/><mergeCell ref=\"H4:K4\"/><mergeCell ref=\"L4:O4\"/>"; xml += "<mergeCell ref=\"P4:Q4\"/><mergeCell ref=\"R4:S4\"/><mergeCell ref=\"T4:V4\"/><mergeCell ref=\"W4:Y4\"/>"; xml += "<mergeCell ref=\"Z4:AA4\"/><mergeCell ref=\"AB4:AD4\"/><mergeCell ref=\"AE4:AH4\"/><mergeCell ref=\"AI4:AN4\"/>"; xml += "<mergeCell ref=\"A5:B5\"/><mergeCell ref=\"C5:G5\"/><mergeCell ref=\"H5:K5\"/><mergeCell ref=\"L5:O5\"/>"; xml += "<mergeCell ref=\"P5:R5\"/><mergeCell ref=\"S5:X5\"/><mergeCell ref=\"Z5:AN5\"/>"; xml += "<mergeCell ref=\"A6:B6\"/><mergeCell ref=\"C6:O6\"/><mergeCell ref=\"P6:R6\"/><mergeCell ref=\"S6:AD6\"/><mergeCell ref=\"AE6:AH6\"/><mergeCell ref=\"AI6:AN6\"/>"; xml += "<mergeCell ref=\"A7:D7\"/><mergeCell ref=\"E7:G7\"/><mergeCell ref=\"H7:J7\"/><mergeCell ref=\"K7:L7\"/>"; xml += "<mergeCell ref=\"M7:O7\"/><mergeCell ref=\"P7:R7\"/><mergeCell ref=\"S7:U7\"/><mergeCell ref=\"V7:X7\"/>"; xml += "<mergeCell ref=\"Y7:Z7\"/><mergeCell ref=\"AA7:AD7\"/><mergeCell ref=\"AE7:AH7\"/><mergeCell ref=\"AI7:AN7\"/>"; xml += "<mergeCell ref=\"A8:C8\"/><mergeCell ref=\"D8:AD8\"/><mergeCell ref=\"AE8:AH8\"/><mergeCell ref=\"AI8:AJ8\"/><mergeCell ref=\"AK8:AL8\"/><mergeCell ref=\"AM8:AN8\"/>"; xml += "<mergeCell ref=\"A" + std::to_string(footerRow) + ":D" + std::to_string(footerRow) + "\"/>"; xml += "<mergeCell ref=\"E" + std::to_string(footerRow) + ":J" + std::to_string(footerRow) + "\"/>"; xml += "<mergeCell ref=\"K" + std::to_string(footerRow) + ":O" + std::to_string(footerRow) + "\"/>"; xml += "<mergeCell ref=\"P" + std::to_string(footerRow) + ":U" + std::to_string(footerRow) + "\"/>"; xml += "<mergeCell ref=\"V" + std::to_string(footerRow) + ":AC" + std::to_string(footerRow) + "\"/>"; xml += "<mergeCell ref=\"AD" + std::to_string(footerRow) + ":AG" + std::to_string(footerRow) + "\"/>"; xml += "</mergeCells>"; xml += "<printOptions horizontalCentered=\"1\"/>"; xml += "<pageMargins left=\"0.15748031496062992\" right=\"0.15748031496062992\" top=\"0.39370078740157483\" " "bottom=\"0.19685039370078741\" header=\"0.31496062992125984\" footer=\"0.11811023622047245\"/>"; xml += "<pageSetup paperSize=\"9\" scale=\"81\" orientation=\"landscape\" horizontalDpi=\"300\" verticalDpi=\"300\" r:id=\"rId1\"/>"; xml += "<headerFooter><oddHeader>&R&\"\xe5\xae\x8b\xe4\xbd\x93,\xe5\x8a\xa0\xe7\xb2\x97\"&8&KFF0000&P/&N</oddHeader></headerFooter>"; xml += "<drawing r:id=\"rId2\"/>"; xml += "</worksheet>"; return xml; } // 统计.rows.tmp里的明细行数(数'\n'个数)。总行数N被footerRow/dimension/ // autoFilter/mergeCells都要用到,先用一次廉价的整块扫描数出来,不必把 // 全部行读进内存 static bool CountRowsInFile(const std::string& rowsPath, int& n, std::string& err) { std::ifstream f; OpenIn(f, rowsPath); if (!f) { err = "rows temp file missing (APPEND阶段可能没有成功执行)"; return false; } n = 0; std::vector<char> buf(1 << 16); while (f) { f.read(buf.data(), (std::streamsize)buf.size()); std::streamsize got = f.gcount(); for (std::streamsize i = 0; i < got; i++) if (buf[i] == '\n') n++; } return true; } // V10 流式克隆:除sheet1.xml外的zip条目原样拷贝;sheet1.xml用STORED(不压缩) // 边生成边写盘--表头一段、明细逐行、合计行+收尾一段,全程只占"一行"的内存。 // STORED需要在local header里写CRC和大小,但流式时写之前还不知道,所以先写占位 // 的local header,边写边增量算CRC/累计字节数,写完seek回去把crc/compSize/ // uncompSize三个字段回填。内存占用与行数无关,支持20万+行不爆内存。 static bool CloneZipStreamingSheet(const std::string& tplPath, const std::string& outPath, const std::string& rowsPath, const std::vector<std::string>& header, const std::vector<std::string>& totals, int N, std::string& err) { std::vector<unsigned char> tpl; if (!ReadWholeFile(tplPath, tpl, err)) return false; std::vector<ZipEntryInfo> entries; if (!ReadZipEntries(tpl, entries, err)) return false; uint16_t sheetModTime = 0, sheetModDate = 0; bool foundSheet = false; for (auto& e : entries) { if (e.name == "xl/worksheets/sheet1.xml") { sheetModTime = e.modTime; sheetModDate = e.modDate; foundSheet = true; break; } } if (!foundSheet) { err = "template missing xl/worksheets/sheet1.xml"; return false; } std::ofstream out; OpenOut(out, outPath, std::ios::trunc); if (!out) { err = "cannot create output file: " + outPath; return false; } struct WrittenEntry { std::string name; uint16_t method, modTime, modDate; uint32_t crc, compSize, uncompSize, offset; }; std::vector<WrittenEntry> written; written.reserve(entries.size()); // 除下面这几个外的其余条目:原始压缩字节整体拷贝 // - sheet1.xml:后面流式重新生成 // - calcChain.xml:V11丢弃(计算链缓存,新表全是纯值没有公式,留着会让Excel // 判定不一致而弹"修复/删除calcChain公式") // - [Content_Types].xml / xl/_rels/workbook.xml.rels:V11改用下面去掉calcChain // 引用的干净版(不管喂进来的模版有没有calcChain,输出都不含、也不悬空引用) for (auto& e : entries) { if (e.name == "xl/worksheets/sheet1.xml") continue; if (e.name == "xl/calcChain.xml") continue; if (e.name == "[Content_Types].xml") continue; if (e.name == "xl/_rels/workbook.xml.rels") continue; size_t spanStart, spanLen; if (!GetLocalEntrySpan(tpl, e, spanStart, spanLen, err)) return false; uint32_t newOffset = (uint32_t)out.tellp(); out.write((const char*)&tpl[spanStart], (std::streamsize)spanLen); written.push_back({ e.name, e.method, e.modTime, e.modDate, e.crc32, e.compSize, e.uncompSize, newOffset }); } // sheet1.xml:流式STORED写入 const std::string sheetName = "xl/worksheets/sheet1.xml"; uint32_t localOff = (uint32_t)out.tellp(); // local header(crc/compSize/uncompSize先写0占位,写完内容再seek回来回填) WriteU32(out, 0x04034b50u); WriteU16(out, 20); WriteU16(out, 0); WriteU16(out, 0); // method=0 STORED WriteU16(out, sheetModTime); WriteU16(out, sheetModDate); WriteU32(out, 0); WriteU32(out, 0); WriteU32(out, 0); // crc, compSize, uncompSize 占位 WriteU16(out, (uint16_t)sheetName.size()); WriteU16(out, 0); out.write(sheetName.data(), (std::streamsize)sheetName.size()); uint32_t crc = 0xFFFFFFFFu; uint32_t total = 0; // 1) 表头(row1-9) { std::string headXml = BuildSheetHead(header, N); out.write(headXml.data(), (std::streamsize)headXml.size()); crc = Crc32Cont(crc, (const unsigned char*)headXml.data(), headXml.size()); total += (uint32_t)headXml.size(); } // 2) 逐行明细(从.rows.tmp一行一行读,一行一行写,内存只留当前行) { std::ifstream rf; OpenIn(rf, rowsPath); if (!rf) { err = "rows temp file missing (APPEND阶段可能没有成功执行)"; return false; } std::string line; line.reserve(4096); std::string rowXml; rowXml.reserve(4096); int idx = 0; while (std::getline(rf, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); std::vector<std::string> fields = SplitUS(line); rowXml.clear(); AppendDataRow(rowXml, fields, 10 + idx); out.write(rowXml.data(), (std::streamsize)rowXml.size()); crc = Crc32Cont(crc, (const unsigned char*)rowXml.data(), rowXml.size()); total += (uint32_t)rowXml.size(); idx++; } } // 3) 合计行 + 收尾 { std::string tailXml = BuildSheetTail(totals, N); out.write(tailXml.data(), (std::streamsize)tailXml.size()); crc = Crc32Cont(crc, (const unsigned char*)tailXml.data(), tailXml.size()); total += (uint32_t)tailXml.size(); } crc ^= 0xFFFFFFFFu; // 回填local header的crc/compSize/uncompSize(偏移14/18/22),再回到文件末尾 out.seekp((std::streamoff)localOff + 14, std::ios::beg); WriteU32(out, crc); WriteU32(out, total); WriteU32(out, total); out.seekp(0, std::ios::end); written.push_back({ sheetName, 0, sheetModTime, sheetModDate, crc, total, total, localOff }); // V11:写去掉calcChain的干净版[Content_Types].xml和xl/_rels/workbook.xml.rels // (STORED)。内容=模版原版删掉calcChain那一行,其余原样。配合上面passthrough // 跳过模版原版+calcChain.xml本体,输出彻底不含calcChain,Excel不再弹修复。 static const std::string CONTENT_TYPES_CLEAN = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">" "<Default Extension=\"bin\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings\"/>" "<Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>" "<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>" "<Default Extension=\"xml\" ContentType=\"application/xml\"/>" "<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>" "<Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>" "<Override PartName=\"/xl/theme/theme1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.theme+xml\"/>" "<Override PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/>" "<Override PartName=\"/xl/sharedStrings.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml\"/>" "<Override PartName=\"/xl/drawings/drawing1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.drawing+xml\"/>" "<Override PartName=\"/docProps/core.xml\" ContentType=\"application/vnd.openxmlformats-package.core-properties+xml\"/>" "<Override PartName=\"/docProps/app.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.extended-properties+xml\"/>" "</Types>"; static const std::string WORKBOOK_RELS_CLEAN = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">" "<Relationship Id=\"rId3\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>" "<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme\" Target=\"theme/theme1.xml\"/>" "<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/>" "<Relationship Id=\"rId4\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings\" Target=\"sharedStrings.xml\"/>" "</Relationships>"; { uint32_t c2 = Crc32(CONTENT_TYPES_CLEAN); uint32_t s2 = (uint32_t)CONTENT_TYPES_CLEAN.size(); uint32_t off2 = (uint32_t)out.tellp(); WriteLocalHeaderAndData(out, "[Content_Types].xml", 0, sheetModTime, sheetModDate, c2, s2, s2, CONTENT_TYPES_CLEAN); written.push_back({ "[Content_Types].xml", 0, sheetModTime, sheetModDate, c2, s2, s2, off2 }); } { uint32_t c3 = Crc32(WORKBOOK_RELS_CLEAN); uint32_t s3 = (uint32_t)WORKBOOK_RELS_CLEAN.size(); uint32_t off3 = (uint32_t)out.tellp(); WriteLocalHeaderAndData(out, "xl/_rels/workbook.xml.rels", 0, sheetModTime, sheetModDate, c3, s3, s3, WORKBOOK_RELS_CLEAN); written.push_back({ "xl/_rels/workbook.xml.rels", 0, sheetModTime,sheetModDate, c3, s3, s3, off3 }); } // 中央目录 + EOCD uint32_t cdStart = (uint32_t)out.tellp(); for (auto& w : written) { WriteU32(out, 0x02014b50u); WriteU16(out, 20); WriteU16(out, 20); WriteU16(out, 0); WriteU16(out, w.method); WriteU16(out, w.modTime); WriteU16(out, w.modDate); WriteU32(out, w.crc); WriteU32(out, w.compSize); WriteU32(out, w.uncompSize); WriteU16(out, (uint16_t)w.name.size()); WriteU16(out, 0); WriteU16(out, 0); WriteU16(out, 0); WriteU16(out, 0); WriteU32(out, 0); WriteU32(out, w.offset); out.write(w.name.data(), (std::streamsize)w.name.size()); } uint32_t cdSize = (uint32_t)((uint32_t)out.tellp() - cdStart); WriteU32(out, 0x06054b50u); WriteU16(out, 0); WriteU16(out, 0); WriteU16(out, (uint16_t)written.size()); WriteU16(out, (uint16_t)written.size()); WriteU32(out, cdSize); WriteU32(out, cdStart); WriteU16(out, 0); out.close(); return true; } // ==================================================================== // 7. 状态持久化(meta/rows两个临时文件,配合INIT/APPEND/FINAL三阶段 // 各自独立调用、没有跨调用内存状态的限制) // ==================================================================== static std::string MetaPath(const std::string& outPath) { return outPath + ".meta.tmp"; } static std::string RowsPath(const std::string& outPath) { return outPath + ".rows.tmp"; } static bool WriteMeta(const std::string& outPath, const std::string& tplPath, int totalCounts, const std::vector<std::string>& header, std::string& err) { std::ofstream f; OpenOut(f, MetaPath(outPath), std::ios::trunc); if (!f) { err = "cannot create meta temp file"; return false; } f << tplPath << FIELD_DELIM << totalCounts << "\n"; f << JoinUS(header) << "\n"; return true; } static bool ReadMeta(const std::string& outPath, std::string& tplPath, int& totalCounts, std::vector<std::string>& header, std::string& err) { std::ifstream f; OpenIn(f, MetaPath(outPath)); if (!f) { err = "meta temp file missing (INIT阶段可能没有成功执行)"; return false; } std::stringstream ss; ss << f.rdbuf(); std::vector<std::string> lines = SplitLines(ss.str()); if (lines.empty()) { err = "meta temp file empty"; return false; } auto l0 = SplitUS(lines[0]); if (l0.size() < 2) { err = "meta line1 bad"; return false; } tplPath = l0[0]; totalCounts = atoi(l0[1].c_str()); if (lines.size() > 1) header = SplitUS(lines[1]); return true; } static bool AppendRows(const std::string& outPath, const std::vector<std::vector<std::string>>& rows, std::string& err) { std::ofstream f; OpenOut(f, RowsPath(outPath), std::ios::app); if (!f) { err = "cannot open rows temp file"; return false; } for (auto& r : rows) f << JoinUS(r) << "\n"; return true; } static bool ReadAllRows(const std::string& outPath, std::vector<std::vector<std::string>>& rows, std::string& err) { std::ifstream f; OpenIn(f, RowsPath(outPath)); if (!f) { err = "rows temp file missing (APPEND阶段可能没有成功执行)"; return false; } std::stringstream ss; ss << f.rdbuf(); std::vector<std::string> lines = SplitLines(ss.str()); for (auto& l : lines) rows.push_back(SplitUS(l)); return true; } static void RemoveFileQuiet(const std::string& path) { RemovePath(path); } // ==================================================================== // 8. INIT / APPEND / FINAL // ==================================================================== static int DoInit(const ExportJob& job, char* ret) { // 校验模版文件能打开 std::ifstream tf; OpenIn(tf, job.tplPath); if (!tf) { SafeCopyRet(ret, 500, "INIT失败: 模版文件打开失败, path=" + job.tplPath); return 0; } tf.close(); std::string err; if (!WriteMeta(job.outPath, job.tplPath, job.totalCounts, job.header, err)) { SafeCopyRet(ret, 500, "INIT失败: " + err); return 0; } // 建一个空的行累积文件(trunc覆盖掉可能存在的旧文件) std::ofstream rf; OpenOut(rf, RowsPath(job.outPath), std::ios::trunc); if (!rf) { SafeCopyRet(ret, 500, "INIT失败: 无法创建行数据临时文件"); return 0; } rf.close(); SafeCopyRet(ret, 500, "init ok, totalCounts=" + std::to_string(job.totalCounts)); return 1; } // ==================================================================== // V12(hex Unicode传输):APPEND阶段每行在SQL端已整行编码成UTF-16LE的十六进制 // (CONVERT(VARCHAR(MAX),CAST(row_line AS VARBINARY(MAX)),2))。原因:脚本引擎的 // db_res()把SQL的NVARCHAR转成客户端本地代码页(中文Windows=GBK/cp936)再返回, // 越南文等非GBK字符会被替换成'?',不可逆丢失。十六进制是纯ASCII,能无损穿过这道 // 边界;这里解码回UTF-16LE再转UTF-8,中文+越南文等所有Unicode字符全部保留。 // 十六进制里不可能含换行,天然不会再出现"字段换行拆行错位/幻影行"。 // 纯手写、不依赖平台API(wchar_t在Win是2字节、Linux是4字节,故用整数), // 32位DLL与64位测试入口通用。 // ==================================================================== static std::string HexUtf16LeToUtf8(const std::string& hex) { auto hv = [](char c)->int { if (c>='0'&&c<='9') return c-'0'; if (c>='A'&&c<='F') return c-'A'+10; if (c>='a'&&c<='f') return c-'a'+10; return -1; }; std::string bytes; bytes.reserve(hex.size()/2); for (size_t i=0; i+1<hex.size(); i+=2) { int h=hv(hex[i]), l=hv(hex[i+1]); if (h<0||l<0) continue; bytes.push_back((char)((h<<4)|l)); } std::string out; out.reserve(bytes.size()); size_t n = bytes.size()/2; for (size_t i=0; i<n; ) { unsigned int u = (unsigned char)bytes[2*i] | ((unsigned int)(unsigned char)bytes[2*i+1] << 8); i++; unsigned int cp = u; if (u>=0xD800 && u<=0xDBFF && i<n) { unsignedint u2 = (unsigned char)bytes[2*i] | ((unsigned int)(unsigned char)bytes[2*i+1] << 8);if (u2>=0xDC00 && u2<=0xDFFF) { cp = 0x10000 + ((u-0xD800)<<10) + (u2-0xDC00); i++; } } if (cp<0x80) out.push_back((char)cp); else if (cp<0x800) { out.push_back((char)(0xC0|(cp>>6))); out.push_back((char)(0x80|(cp&0x3F))); } else if (cp<0x10000) { out.push_back((char)(0xE0|(cp>>12))); out.push_back((char)(0x80|((cp>>6)&0x3F))); out.push_back((char)(0x80|(cp&0x3F))); } else { out.push_back((char)(0xF0|(cp>>18))); out.push_back((char)(0x80|((cp>>12)&0x3F))); out.push_back((char)(0x80|((cp>>6)&0x3F))); out.push_back((char)(0x80|(cp&0x3F))); } } return out; } // 测试入口专用:UTF-8 -> UTF-16LE -> 十六进制(仅#else测试用,模拟SQL端编码) static std::string Utf8ToHexUtf16Le(const std::string& utf8) { std::string bytes; auto emit16 = [&](unsigned int u){ bytes.push_back((char)(u&0xFF)); bytes.push_back((char)((u>>8)&0xFF)); }; size_t i=0, N=utf8.size(); while (i<N) { unsigned char c=(unsigned char)utf8[i]; unsigned int cp; int len; if (c<0x80){ cp=c; len=1; } else if ((c>>5)==0x6){ cp=c&0x1F; len=2; } else if ((c>>4)==0xE){ cp=c&0x0F; len=3; } else if ((c>>3)==0x1E){ cp=c&0x07; len=4; } else { cp=c; len=1; } for (int k=1;k<len && i+(size_t)k<N;k++) cp=(cp<<6)|((unsigned char)utf8[i+k]&0x3F); i+=len; if (cp<0x10000) emit16(cp); else { cp-=0x10000; emit16(0xD800+(cp>>10)); emit16(0xDC00+(cp&0x3FF)); } } static const char* H="0123456789ABCDEF"; std::string hex; hex.reserve(bytes.size()*2); for (unsigned char b : bytes){ hex.push_back(H[b>>4]); hex.push_back(H[b&0xF]); } return hex; } static int DoAppend(const ExportJob& job, char* ret) { // 每行是"整行row_line的UTF-16LE十六进制";先解码回UTF-8,再按|~|拆成40字段 std::vector<std::vector<std::string>> decoded; decoded.reserve(job.rows.size()); for (const auto& r : job.rows) { std::string hex = JoinUS(r); // 整行十六进制(hex里无|~|,等于r[0]) std::string utf8 = HexUtf16LeToUtf8(hex); // -> "f0|~|f1|~|...|~|f39"(UTF-8) decoded.push_back(SplitUS(utf8)); } std::string err; if (!AppendRows(job.outPath, decoded, err)) { SafeCopyRet(ret, 500, "APPEND失败: " + err); return 0; } SafeCopyRet(ret, 500, "append ok, offset=" + std::to_string(job.startOffset) + " count=" + std::to_string(job.chunkCount)); return 1; } static int DoFinal(const ExportJob& job, char* ret) { std::string err; std::string tplPath; int totalCounts = 0; std::vector<std::string> header; if (!ReadMeta(job.outPath, tplPath, totalCounts, header, err)) { SafeCopyRet(ret, 500, "FINAL失败: " + err); return 0; } // V10:先数出总行数N(整块扫描.rows.tmp数换行,不把行读进内存), // 再流式逐行写盘生成xlsx。全程内存占用与行数无关,支持20万+行 int N = 0; if (!CountRowsInFile(RowsPath(job.outPath), N, err)) { SafeCopyRet(ret, 500, "FINAL失败: " + err); return 0; } if (!CloneZipStreamingSheet(tplPath, job.outPath, RowsPath(job.outPath), header, job.totals, N, err)) { SafeCopyRet(ret, 500, "FINAL失败(生成xlsx): " + err); return 0; } RemoveFileQuiet(MetaPath(job.outPath)); RemoveFileQuiet(RowsPath(job.outPath)); SafeCopyRet(ret, 500, "final ok, trow=" + std::to_string(10 + N)); return 1; } // ==================================================================== // 【纯净版】客户订单明细表导出模版_无表头和检索内容.xlsx 专用生成 // 布局:第1行=40列列头(样式s=5),第2..1+N行=数据(统一样式s=4), // 第2+N行=合计行(查询人/查询日期/合计,7个合并,样式与纯净模版一致)。 // 列宽/pageSetup/printerSettings/styles 全部沿用纯净模版文件本身 // (克隆时除sheet1.xml外所有条目原样拷贝)。 // 列顺序与主模版差3列:主模版尾部=订单单位,客户订单备注,备注,生产数量(Y);// 纯净模版尾部=订单单位,生产数量(Y),客户订单备注,备注。PLAIN_SQLIDX做SQL列->纯净列映射。 // ==================================================================== static const char* PLAIN_COLS = "<cols>" "<col min=\"1\" max=\"2\" width=\"7.3984375\" customWidth=\"1\"/>" "<col min=\"3\" max=\"3\" width=\"6.09765625\" customWidth=\"1\"/>" "<col min=\"4\" max=\"4\" width=\"7.3984375\" customWidth=\"1\"/>" "<col min=\"5\" max=\"5\" width=\"5.5\" customWidth=\"1\"/>" "<col min=\"6\" max=\"6\" width=\"7.3984375\" customWidth=\"1\"/>" "<col min=\"7\" max=\"7\" width=\"5.3984375\" customWidth=\"1\"/>" "<col min=\"8\" max=\"8\" width=\"7.3984375\" customWidth=\"1\"/>" "<col min=\"9\" max=\"9\" width=\"5.8984375\" customWidth=\"1\"/>" "<col min=\"10\" max=\"10\" width=\"9.19921875\" customWidth=\"1\"/>" "<col min=\"11\" max=\"11\" width=\"5.3984375\" customWidth=\"1\"/>" "<col min=\"12\" max=\"12\" width=\"5.69921875\" customWidth=\"1\"/>" "<col min=\"13\" max=\"14\" width=\"7.19921875\" customWidth=\"1\"/>" "<col min=\"15\" max=\"16\" width=\"5.59765625\" customWidth=\"1\"/>" "<col min=\"17\" max=\"17\" width=\"8.3984375\" customWidth=\"1\"/>" "<col min=\"18\" max=\"18\" width=\"9\" customWidth=\"1\"/>" "<col min=\"19\" max=\"19\" width=\"5.5\" customWidth=\"1\"/>" "<col min=\"20\" max=\"20\" width=\"9.296875\" customWidth=\"1\"/>" "<col min=\"21\" max=\"22\" width=\"10.3984375\" customWidth=\"1\"/>" "<col min=\"23\" max=\"23\" width=\"13.5\" customWidth=\"1\"/>" "<col min=\"24\" max=\"24\" width=\"9.8984375\" customWidth=\"1\"/>" "<col min=\"25\" max=\"25\" width=\"17.8984375\" customWidth=\"1\"/>" "<col min=\"26\" max=\"26\" width=\"7.3984375\" customWidth=\"1\"/>" "<col min=\"27\" max=\"27\" width=\"11.09765625\" customWidth=\"1\"/>" "<col min=\"28\" max=\"28\" width=\"12.296875\" customWidth=\"1\"/>" "<col min=\"29\" max=\"29\" width=\"5.69921875\" customWidth=\"1\"/>""<col min=\"30\" max=\"33\" width=\"6.19921875\" customWidth=\"1\"/>" "<col min=\"34\" max=\"36\" width=\"10.8984375\" customWidth=\"1\"/>" "<col min=\"37\" max=\"37\" width=\"10.09765625\" customWidth=\"1\"/>" "<col min=\"38\" max=\"38\" width=\"13.296875\" customWidth=\"1\"/>" "<col min=\"39\" max=\"39\" width=\"18.69921875\" customWidth=\"1\"/>" "<col min=\"40\" max=\"40\" width=\"19\" customWidth=\"1\"/>" "</cols>"; static const char* kHeadersPlain[40] = { "\xe5\xba\x8f\xe5\x8f\xb7", "\xe8\xae\xa2\xe5\x8d\x95\xe5\x8d\x95\xe6\x8d\xae\xe7\xbc\x96\xe5\x8f\xb7", "\xe5\x8d\x95\xe6\x8d\xae\xe6\x97\xa5\xe6\x9c\x9f", "\xe5\x88\xb6\xe5\x8d\x95\xe4\xba\xba", "\xe5\x8d\x95\xe6\x8d\xae\xe7\x8a\xb6\xe6\x80\x81", "\xe5\xae\xa1\xe6\xa0\xb8\xe4\xba\xba", "\xe5\xae\xa2\xe6\x88\xb7\xe7\xbc\x96\xe5\x8f\xb7", "\xe4\xb8\x9a\xe5\x8a\xa1\xe5\x91\x98", "\xe5\x86\x85\xe9\x83\xa8\xe8\xae\xa2\xe5\x8f\xb7", "\xe8\xae\xa2\xe5\x8d\x95\xe7\xb1\xbb\xe5\x9e\x8b", "\xe7\x94\x9f\xe4\xba\xa7\xe9\x83\xa8\xe9\x97\xa8", "\xe6\x88\x90\xe5\x93\x81\xe9\xa1\xb9\xe6\xac\xa1", "\xe5\xb7\xa5\xe5\x8d\x95\xe5\x8f\xb7", "\xe9\xa2\x84\xe5\x91\x8a\xe5\xb7\xa5\xe5\x8d\x95\xe5\x8f\xb7", "\xe4\xb8\x8b\xe5\x8d\x95\xe6\x97\xa5\xe6\x9c\x9f", "\xe4\xba\xa4\xe4\xbb\x98\xe6\x97\xa5\xe6\x9c\x9f", "\xe5\xae\xa2\xe6\x88\xb7\xe9\x87\x87\xe8\xb4\xad\xe5\x8d\x95\xe5\x8f\xb7", "\xe5\xae\xa2\xe6\x88\xb7\xe5\x9e\x8b\xe4\xbd\x93", "\xe9\xa1\xb9\xe6\xac\xa1", "\xe5\xae\xa2\xe6\x88\xb7\xe6\x8c\x87\xe4\xbb\xa4", "\xe4\xbc\x81\xe5\x88\x92\xe5\x8d\x95\xe5\x8f\xb7", "\xe4\xbc\x81\xe4\xb8\x9a\xe5\x8d\x95\xe5\x8f\xb7", "\xe5\xae\xa2\xe6\x88\xb7\xe7\x89\xa9\xe6\x96\x99\xe7\xbc\x96\xe5\x8f\xb7", "\xe5\x86\x85\xe9\x83\xa8\xe6\x96\x99\xe5\x8f\xb7", "\xe5\x93\x81\xe5\x90\x8d\xe6\x8f\x8f\xe8\xbf\xb0", "\xe8\xa7\x84\xe6\xa0\xbc", "\xe9\x80\x9a\xe7\x94\xa8\xe8\x89\xb2\xe5\x8f\xb7", "\xe5\x86\x85\xe9\x83\xa8\xe8\x89\xb2\xe5\x8f\xb7", "\xe9\x95\xbf\xe5\xba\xa6", "\xe6\x98\xaf\xe5\x90\xa6\xe6\x89\x93\xe7\xa1\xae\xe8\xae\xa4\xe6\xa0\xb7", "\xe6\x98\xaf\xe5\x90\xa6\xe9\x98\xb2\xe8\x99\xb9\xe5\x90\xb8", "\xe8\xae\xa2\xe8\xb4\xad\xe8\x83\xb6\xe7\x89\x87", "\xe7\xbb\x87\xe5\xb8\xa6\xe5\x8d\xb0\xe5\x88\xb7", "\xe8\xae\xa2\xe5\x8d\x95\xe6\x95\xb0\xe9\x87\x8f", "\xe5\xb7\xb2\xe5\x87\xba\xe8\xb4\xa7\xe6\x95\xb0\xe9\x87\x8f", "\xe5\xbe\x85\xe5\x87\xba\xe8\xb4\xa7\xe6\x95\xb0\xe9\x87\x8f", "\xe8\xae\xa2\xe5\x8d\x95\xe5\x8d\x95\xe4\xbd\x8d", "\xe7\x94\x9f\xe4\xba\xa7\xe6\x95\xb0\xe9\x87\x8f(Y)", "\xe5\xae\xa2\xe6\x88\xb7\xe8\xae\xa2\xe5\x8d\x95\xe5\xa4\x87\xe6\xb3\xa8", "\xe5\xa4\x87\xe6\xb3\xa8" }; static const char* PLAIN_LBL_PERSON = "\xe6\x9f\xa5\xe8\xaf\xa2\xe4\xba\xba \x0aNg\xc6\xb0\xe1\xbb\x9di truy v\xe1\xba\xa5n\xef\xbc\x9a"; static const char* PLAIN_LBL_DATE = "\xe6\x9f\xa5\xe8\xaf\xa2\xe6\x97\xa5\xe6\x9c\x9f \x0aNg\xc3\xa0y truy v\xe1\xba\xa5n\xef\xbc\x9a"; static const char* PLAIN_LBL_TOTAL = "\xe5\x90\x88\xe8\xae\xa1\xef\xbc\x9a\x0aT\xe1\xbb\x95ng c\xe1\xbb\x99ng"; // 纯净列(1-based) -> SQL字段下标(0-based):1..37同序;38<-SQL40(生产数量Y);39<-SQL38(客户订单备注);40<-SQL39(备注) static const int PLAIN_SQLIDX[40] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18, 19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36, 39,37,38 }; // 纯净版表头(第1行)+ PRE(dimension/冻结/列宽/sheetData开) static std::string BuildPlainSheetHead(int N) { int footerRow = 2 + N; std::string xml; xml.reserve(8000); xml += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n"; xml += "<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " "xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" " "xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" " "xmlns:x14ac=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac\" " "mc:Ignorable=\"x14ac\">"; xml += "<dimension ref=\"A1:AN" + std::to_string(footerRow) + "\"/>"; xml += "<sheetViews><sheetView tabSelected=\"1\" zoomScaleNormal=\"100\" zoomScaleSheetLayoutView=\"100\" " "workbookViewId=\"0\"><pane ySplit=\"1\" topLeftCell=\"A2\" activePane=\"bottomLeft\" state=\"frozenSplit\"/>" "<selection pane=\"bottomLeft\" activeCell=\"A2\" sqref=\"A2\"/></sheetView></sheetViews>"; xml += "<sheetFormatPr defaultRowHeight=\"15.6\" x14ac:dyDescent=\"0.25\"/>"; xml += PLAIN_COLS; xml += "<sheetData>"; xml += "<row r=\"1\" spans=\"1:40\" s=\"1\" customFormat=\"1\" ht=\"46.8\" x14ac:dyDescent=\"0.25\">"; for (int c = 1; c <= 40; c++) xml += CellInlineStr(ColLetter(c) + "1", "5", kHeadersPlain[c - 1]); xml += "</row>"; return xml; } // 纯净版数据行(第r行,40列,统一s=4;数字列1/34/35/36/38用<v>,按SQL列映射取值) static void AppendPlainDataRow(std::string& xml, const std::vector<std::string>& sqlFields, int r) { xml += "<row r=\"" + std::to_string(r) + "\" spans=\"1:40\" x14ac:dyDescent=\"0.25\">"; for (int c = 1; c <= 40; c++) { std::string ref = ColLetter(c) + std::to_string(r); int si = PLAIN_SQLIDX[c - 1]; std::string val = (si < (int)sqlFields.size()) ? sqlFields[si] : std::string(); if (c == 1 || c == 34 || c == 35 || c == 36 || c == 38) xml += CellNum(ref, "4", val); else xml += CellInlineStr(ref, "4", val); } xml += "</row>"; } // 纯净版合计行(第2+N行)+ </sheetData> + 合并 + pageSetup(沿用纯净模版) + 收尾 static std::string BuildPlainSheetTail(const std::vector<std::string>& totals, int N) { auto T = [&](size_t i) -> std::string { return (i < totals.size()) ? totals[i] : std::string(); }; int fr = 2 + N; std::string f = std::to_string(fr); std::string xml; xml.reserve(4000); xml += "<row r=\"" +f + "\" spans=\"1:40\" ht=\"25.2\" customHeight=\"1\" x14ac:dyDescent=\"0.25\">"; xml += CellInlineStr("A" + f, "9", PLAIN_LBL_PERSON); xml += "<c r=\"B" + f + "\" s=\"10\"/><c r=\"C" + f + "\" s=\"11\"/>"; xml += CellInlineStr("D" + f, "12", T(0)); xml += "<c r=\"E" + f + "\" s=\"13\"/><c r=\"F" + f + "\" s=\"13\"/><c r=\"G" + f + "\" s=\"13\"/><c r=\"H" + f + "\" s=\"13\"/><c r=\"I" + f + "\" s=\"13\"/><c r=\"J" + f + "\" s=\"14\"/>"; xml += CellInlineStr("K" + f, "9", PLAIN_LBL_DATE); xml += "<c r=\"L" + f + "\" s=\"10\"/><c r=\"M" + f + "\" s=\"11\"/>"; xml += CellInlineStr("N" + f, "15", T(1)); xml += "<c r=\"O" + f + "\" s=\"16\"/><c r=\"P" + f + "\" s=\"16\"/><c r=\"Q" + f + "\" s=\"16\"/><c r=\"R" + f + "\" s=\"17\"/>"; xml += "<c r=\"S" + f + "\" s=\"8\"/><c r=\"T" + f + "\" s=\"8\"/><c r=\"U" + f + "\" s=\"8\"/>"; xml += "<c r=\"V" + f + "\" s=\"3\"/><c r=\"W" + f + "\" s=\"3\"/><c r=\"X" + f + "\" s=\"3\"/><c r=\"Y" + f + "\" s=\"3\"/><c r=\"Z" + f + "\" s=\"3\"/><c r=\"AA" + f + "\" s=\"3\"/><c r=\"AB" + f + "\" s=\"3\"/><c r=\"AC" + f + "\" s=\"3\"/>"; xml += "<c r=\"AE" + f + "\" s=\"18\"/>"; xml += CellInlineStr("AF" + f, "19", PLAIN_LBL_TOTAL); xml += "<c r=\"AG" + f + "\" s=\"20\"/>"; xml += CellNum("AH" + f, "2", T(2)); xml += CellNum("AI" + f, "2", T(3)); xml += CellNum("AJ" + f, "2", T(4)); xml += "<c r=\"AK" + f + "\" s=\"21\"/>"; xml += CellNum("AL" + f, "2", T(5)); xml += "<c r=\"AM" + f + "\" s=\"6\"/><c r=\"AN" + f + "\" s=\"7\"/>"; xml += "</row>"; xml += "</sheetData>"; xml += "<mergeCells count=\"7\">"; xml += "<mergeCell ref=\"A" + f + ":C" + f + "\"/><mergeCell ref=\"D" + f + ":J" + f + "\"/><mergeCell ref=\"K" + f + ":M" + f + "\"/><mergeCell ref=\"N" + f + ":R" + f + "\"/><mergeCell ref=\"AF" + f + ":AG" + f + "\"/><mergeCell ref=\"AM" + f + ":AN" + f + "\"/><mergeCell ref=\"V" + f + ":AC" + f + "\"/>"; xml += "</mergeCells>"; xml += "<phoneticPr fontId=\"19\" type=\"noConversion\"/>"; xml += "<printOptions horizontalCentered=\"1\"/>"; xml += "<pageMargins left=\"0.15748031496062992\" right=\"0.15748031496062992\" top=\"0.39370078740157483\" bottom=\"0.19685039370078741\" header=\"0.31496062992125984\" footer=\"0.11811023622047245\"/>"; xml += "<pageSetup paperSize=\"8\" scale=\"81\" orientation=\"landscape\" horizontalDpi=\"300\" verticalDpi=\"300\" r:id=\"rId1\"/>"; xml += "<headerFooter><oddHeader>&R&\"\xe5\xae\x8b\xe4\xbd\x93,\xe5\x8a\xa0\xe7\xb2\x97\"&8&KFF0000&P/&N</oddHeader></headerFooter>"; xml += "</worksheet>"; return xml; } // 克隆纯净模版:除sheet1.xml外所有条目原样拷贝(纯净模版无calcChain,Content_Types/ // workbook.rels/printerSettings/styles全部照搬),sheet1.xml按上面纯净布局流式STORED生成。 static bool CloneZipPlainSheet(const std::string& tplPath, const std::string& outPath, const std::string& rowsPath, const std::vector<std::string>& totals, int N, std::string& err) { std::vector<unsigned char> tpl; if (!ReadWholeFile(tplPath, tpl, err)) return false; std::vector<ZipEntryInfo> entries; if (!ReadZipEntries(tpl, entries, err)) return false; uint16_t sheetModTime = 0, sheetModDate = 0; bool foundSheet = false; for (auto& e : entries) { if (e.name == "xl/worksheets/sheet1.xml") { sheetModTime = e.modTime; sheetModDate = e.modDate; foundSheet = true; break; } } if (!foundSheet) { err = "plain template missing xl/worksheets/sheet1.xml"; return false; } std::ofstream out; OpenOut(out, outPath, std::ios::trunc); if (!out) { err = "cannot create plain output file: " + outPath; return false; } struct WrittenEntry { std::string name; uint16_t method, modTime, modDate; uint32_t crc, compSize, uncompSize, offset; }; std::vector<WrittenEntry> written; written.reserve(entries.size()); for (auto& e : entries) { if (e.name == "xl/worksheets/sheet1.xml") continue; size_t spanStart, spanLen; if (!GetLocalEntrySpan(tpl, e, spanStart, spanLen, err)) return false; uint32_t newOffset = (uint32_t)out.tellp(); out.write((const char*)&tpl[spanStart], (std::streamsize)spanLen); written.push_back({ e.name, e.method, e.modTime, e.modDate, e.crc32, e.compSize, e.uncompSize, newOffset }); } const std::string sheetName = "xl/worksheets/sheet1.xml"; uint32_t localOff = (uint32_t)out.tellp(); WriteU32(out, 0x04034b50u); WriteU16(out, 20); WriteU16(out, 0); WriteU16(out, 0); WriteU16(out, sheetModTime); WriteU16(out, sheetModDate); WriteU32(out, 0); WriteU32(out, 0); WriteU32(out, 0); WriteU16(out, (uint16_t)sheetName.size()); WriteU16(out, 0); out.write(sheetName.data(), (std::streamsize)sheetName.size()); uint32_t crc = 0xFFFFFFFFu, total = 0; { std::string head = BuildPlainSheetHead(N); out.write(head.data(), (std::streamsize)head.size()); crc = Crc32Cont(crc, (const unsigned char*)head.data(), head.size()); total += (uint32_t)head.size(); } { std::ifstream rf; OpenIn(rf, rowsPath); if (!rf) { err = "rows temp missing (plain)"; return false; } std::string line; line.reserve(4096); std::string rowXml; rowXml.reserve(4096); int idx = 0; while (std::getline(rf, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); std::vector<std::string> fields = SplitUS(line); rowXml.clear(); AppendPlainDataRow(rowXml, fields, 2 + idx); out.write(rowXml.data(), (std::streamsize)rowXml.size()); crc = Crc32Cont(crc, (const unsigned char*)rowXml.data(), rowXml.size()); total += (uint32_t)rowXml.size(); idx++; } } { std::string tail = BuildPlainSheetTail(totals, N); out.write(tail.data(), (std::streamsize)tail.size()); crc = Crc32Cont(crc, (const unsigned char*)tail.data(), tail.size()); total += (uint32_t)tail.size(); } crc ^= 0xFFFFFFFFu; out.seekp((std::streamoff)localOff + 14, std::ios::beg); WriteU32(out, crc); WriteU32(out, total); WriteU32(out, total); out.seekp(0, std::ios::end); written.push_back({ sheetName, 0, sheetModTime, sheetModDate, crc, total, total, localOff }); uint32_t cdStart = (uint32_t)out.tellp(); for (auto& w : written) { WriteU32(out, 0x02014b50u); WriteU16(out, 20); WriteU16(out, 20); WriteU16(out, 0); WriteU16(out, w.method); WriteU16(out, w.modTime); WriteU16(out, w.modDate); WriteU32(out, w.crc); WriteU32(out, w.compSize); WriteU32(out, w.uncompSize); WriteU16(out, (uint16_t)w.name.size()); WriteU16(out, 0); WriteU16(out, 0); WriteU16(out, 0); WriteU16(out, 0); WriteU32(out, 0); WriteU32(out, w.offset); out.write(w.name.data(), (std::streamsize)w.name.size()); } uint32_t cdSize = (uint32_t)((uint32_t)out.tellp() - cdStart); WriteU32(out, 0x06054b50u); WriteU16(out, 0); WriteU16(out, 0); WriteU16(out, (uint16_t)written.size()); WriteU16(out, (uint16_t)written.size()); WriteU32(out, cdSize); WriteU32(out, cdStart); WriteU16(out, 0); out.close(); return true; } // ==================== 运行时模版引擎(v15) ==================== // 精简 DEFLATE 解压器(RFC1951 inflate,仅解压,无外部依赖)。 // 用于运行时读取 Excel 模版内被 DEFLATE 压缩的 sheet1.xml / sharedStrings.xml / workbook.xml。 // 返回 true 并把解压结果放进 out;失败返回 false。 #include <vector> #include <cstring> #include <cstdint> struct InfState { const unsigned char* in; size_t inlen; size_t inpos; unsigned bitbuf; intbitcnt; std::vector<unsigned char>* out; }; static int inf_bits(InfState* s, int need) { unsigned val = s->bitbuf; while (s->bitcnt < need) { if (s->inpos >= s->inlen) return -1; val |= (unsigned)(s->in[s->inpos++]) << s->bitcnt; s->bitcnt += 8; } s->bitbuf = val >> need; s->bitcnt -= need; return (int)(val & ((1u << need) - 1)); } struct InfHuff { std::vector<short> count; std::vector<short> symbol; }; static int inf_decode(InfState* s, const InfHuff* h) { int code = 0, first = 0, index = 0; for (int len = 1; len <= 15; len++) { int b = inf_bits(s, 1); if (b < 0) return -1; code |= b; int cnt = h->count[len]; if (code - first < cnt) return h->symbol[index + (code - first)]; index += cnt; first += cnt; first <<= 1; code <<= 1; } return -1; } static void inf_construct(InfHuff* h, const unsigned char* lengths, int n) { h->count.assign(16, 0); h->symbol.assign(n, 0); for (int i = 0; i < n; i++) h->count[lengths[i]]++; h->count[0] = 0; std::vector<short> offs(16, 0); for (int len = 1; len < 16; len++) offs[len] = offs[len-1] + h->count[len-1]; for (int i = 0; i < n; i++) if (lengths[i]) h->symbol[offs[lengths[i]]++] = (short)i; } static int inf_codes(InfState* s, const InfHuff* lh, const InfHuff* dh) { static const short lens[29] = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258}; static const short lext[29] = {0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0}; static const short dists[30] = {1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577}; static const short dext[30] = {0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; for (;;) { int sym = inf_decode(s, lh); if (sym < 0) return -1; if (sym == 256) return 0; if (sym < 256) { s->out->push_back((unsigned char)sym); } else { sym -= 257; if (sym >= 29) return -1; int e = inf_bits(s, lext[sym]); if (e <0) return -1; int length = lens[sym] + e; int dsym = inf_decode(s, dh); if (dsym < 0 || dsym >= 30) return -1; int de = inf_bits(s, dext[dsym]); if (de < 0) return -1; int dist = dists[dsym] + de; if ((size_t)dist > s->out->size()) return -1; size_t start = s->out->size() - dist; for (int i = 0; i < length; i++) s->out->push_back((*s->out)[start + i]); } } } static int inf_fixed(InfState* s) { unsigned char ll[288]; int i=0; for (; i<144; i++) ll[i]=8; for (; i<256; i++) ll[i]=9; for (; i<280; i++) ll[i]=7; for (; i<288; i++) ll[i]=8; unsigned char dl[30]; for (i=0;i<30;i++) dl[i]=5; InfHuff lh, dh; inf_construct(&lh, ll, 288); inf_construct(&dh, dl, 30); return inf_codes(s, &lh, &dh); } static int inf_dynamic(InfState* s) { static const short ord[19] = {16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15}; int hlit = inf_bits(s,5); int hdist = inf_bits(s,5); int hclen = inf_bits(s,4); if (hlit<0||hdist<0||hclen<0) return -1; hlit += 257; hdist += 1; hclen += 4; unsigned char clens[19]; memset(clens,0,sizeof(clens)); for (int i=0;i<hclen;i++){ int b=inf_bits(s,3); if(b<0)return -1; clens[ord[i]]=(unsigned char)b; } InfHuff ch; inf_construct(&ch, clens, 19); unsigned char lengths[288+32]; int idx=0; int total=hlit+hdist; while (idx < total) { int sym = inf_decode(s, &ch); if (sym<0) return -1; if (sym < 16) lengths[idx++] = (unsigned char)sym; else if (sym == 16) { if(idx==0)return -1; int r=inf_bits(s,2); if(r<0)return -1; r+=3; unsigned char prev=lengths[idx-1]; while(r-- && idx<total) lengths[idx++]=prev; } else if (sym == 17) { int r=inf_bits(s,3); if(r<0)return -1; r+=3; while(r-- && idx<total) lengths[idx++]=0; } else { int r=inf_bits(s,7); if(r<0)return -1; r+=11; while(r-- && idx<total) lengths[idx++]=0; } } if (idx != total) return-1; InfHuff lh, dh; inf_construct(&lh, lengths, hlit); inf_construct(&dh, lengths+hlit, hdist); return inf_codes(s, &lh, &dh); } static bool InflateRaw(const unsigned char* data, size_t len, std::vector<unsigned char>& out) { InfState s; s.in=data; s.inlen=len; s.inpos=0; s.bitbuf=0; s.bitcnt=0; s.out=&out; int last; do { last = inf_bits(&s, 1); if (last<0) return false; int type = inf_bits(&s, 2); if (type<0) return false; if (type == 0) { s.bitbuf=0; s.bitcnt=0; if (s.inpos+4 > s.inlen) return false; unsigned l = s.in[s.inpos] | (s.in[s.inpos+1]<<8); s.inpos+=4; if (s.inpos+l > s.inlen) return false; for (unsigned i=0;i<l;i++) out.push_back(s.in[s.inpos++]); } else if (type == 1) { if (inf_fixed(&s)!=0) return false; } else if (type == 2) { if (inf_dynamic(&s)!=0) return false; } else return false; } while (!last); return true; } // 运行时"读模版"引擎:读带占位符的样板模版,按数据生成最终 sheet1.xml。 // 占位符: =第k个SQL字段(数据行,标记原型行); =第k个条件回显; // =查询人 =查询日期 =四个合计。 // 保留模版一切(列宽/样式/表名/页面/logo/隔行底色条件格式),只:替换占位符、 // 按原型行逐条生成数据行、合计行整体下移、修正 dimension/冻结/合并/筛选/条件格式范围。 #include <string> #include <vector> #include <regex> #include <set> #include <cstdio> static const std::set<int> ENG_NUMERIC = {0,33,34,35,39}; // 序号,订单数量,已出货,待出货,生产数量(Y) // XML安全清洗:校验UTF-8,丢弃/替换 控制字符(<0x20)、非法UTF-8(孤立代理项/过长/截断/超范围)、 // XML非法码点(0xFFFE/0xFFFF)--一律换空格;并转义 & < >。ERP脏数据里的损坏Unicode会导致 // sheet1.xml非法、Excel弹"修复",这里统一兜底。 static std::string EngEsc(const std::string& s){ std::string o; o.reserve(s.size()+8); size_t i=0, n=s.size(); while(i<n){ unsigned char c=(unsignedchar)s[i]; if(c<0x20){ o+=' '; i++; continue; } if(c<0x80){ if(c=='&')o+="&"; else if(c=='<')o+="<"; else if(c=='>')o+=">"; else o+=(char)c; i++; continue; } int len; unsigned cp; if((c&0xE0)==0xC0){ len=2; cp=c&0x1F; } else if((c&0xF0)==0xE0){ len=3; cp=c&0x0F; } else if((c&0xF8)==0xF0){ len=4; cp=c&0x07; } else { o+=' '; i++; continue; } // 非法前导字节 if(i+(size_t)len>n){ o+=' '; i++; continue; } // 截断 bool ok=true; for(int k=1;k<len;k++){ unsigned char cc=(unsigned char)s[i+k]; if((cc&0xC0)!=0x80){ ok=false; break; } cp=(cp<<6)|(cc&0x3F); } if(!ok){ o+=' '; i++; continue; } // 续字节非法 bool valid=true; if(len==2 && cp<0x80) valid=false; else if(len==3 && cp<0x800) valid=false; else if(len==4 && cp<0x10000) valid=false; // 过长编码 if(cp>=0xD800 && cp<=0xDFFF) valid=false; // 代理项 if(cp>0x10FFFF) valid=false; // 超范围 if(cp==0xFFFE || cp==0xFFFF) valid=false; // XML非法码点 if(!valid){ o+=' '; i++; continue; } o.append(s, i, len); i+=len; } return o; } static bool EngIsNum(const std::string& v){ if(v.empty())return false; for(char c:v) if(!((c>='0'&&c<='9')||c=='-'||c=='.')) return false; return true; } // 取所有 <row ...>...</row> static void EngSplit(const std::string& sh, std::string& pre, std::vector<std::string>& rows, std::string& post){ size_t sd=sh.find("<sheetData>"); size_t se=sh.find("</sheetData>"); pre=sh.substr(0, sd+std::string("<sheetData>").size()); std::string mid=sh.substr(sd+std::string("<sheetData>").size(), se-(sd+std::string("<sheetData>").size()));post=sh.substr(se); // 包含 </sheetData> ... 收尾 std::regex rx("<row [^>]*>[\\s\\S]*?</row>|<row [^>]*/>",std::regex::icase); for(auto it=std::sregex_iterator(mid.begin(),mid.end(),rx); it!=std::sregex_iterator(); ++it) rows.push_back(it->str()); } static int EngRowNum(const std::string& row){ std::smatch m; std::regex rx("<row r=\"(\\d+)\""); if(std::regex_search(row,m,rx)) return std::stoi(m[1]); return -1; } // 把一行里所有 r="COL{oldr}" 与 <row r="oldr" 改成 newr。 // 【性能 v317】原用 std::regex_replace,每行每列都重新构造正则(编译极慢),3.9万行 // 累计近50万次正则编译=112秒。这里改纯字符串:扫描每个 r="<列字母><行号>",行号==oldr // 就替换成 newr。<row r="10"> 无列字母也同样处理(列字母段为空)。零正则。 static std::string EngRenumber(const std::string& row, int oldr, int newr){ std::string olds=std::to_string(oldr), news=std::to_string(newr); std::string o; o.reserve(row.size()+16); size_t i=0, n=row.size(); while(i<n){ size_t p=row.find("r=\"", i); if(p==std::string::npos){ o.append(row, i, n-i); break; } o.append(row, i, p+3-i); // 拷贝到并含 r=" size_t j=p+3; size_t letStart=j; while(j<n && row[j]>='A' && row[j]<='Z') j++; // 列字母 [A-Z]* size_t numStart=j; while(j<n && row[j]>='0' && row[j]<='9') j++; // 行号 [0-9]* o.append(row, letStart, numStart-letStart); // 列字母原样 if(j>numStart && row.compare(numStart, j-numStart, olds)==0) o.append(news); else o.append(row, numStart, j-numStart); // 行号(不等则原样) i=j; } return o; } // 替换一个数字占位符所在单元格为 <v> 数字(或空单元格),保留 r/s 属性。 // 【性能 v317】原用 std::regex(每次构造),改纯字符串:定位 token 所在的 // <c ...>…</c>,从开标签取 r="" 与 s="",整格替换成 <c r s><v>val</v></c> 或 <c r s/>。 static std::string EngNumCell(const std::string& row, const std::string& token, const std::string& val){ size_t tp = row.find(token); if(tp==std::string::npos) return row; size_t cs = row.rfind("<c ", tp); if(cs==std::string::npos) return row; size_t ce = row.find("</c>", tp); if(ce==std::string::npos) return row; ce += 4; // 含 </c> size_t tagEnd = row.find('>', cs); if(tagEnd==std::string::npos || tagEnd>tp) return row; std::string tag = row.substr(cs, tagEnd-cs); // 形如 <c r="A10" s="6" t="inlineStr" std::string rAttr, sAttr; size_t rp = tag.find(" r=\""); if(rp!=std::string::npos){ size_t e=tag.find('"', rp+4); if(e!=std::string::npos) rAttr=tag.substr(rp, e-rp+1); } size_t sp = tag.find(" s=\""); if(sp!=std::string::npos){ size_t e=tag.find('"', sp+4); if(e!=std::string::npos) sAttr=tag.substr(sp, e-sp+1); } std::string rep = EngIsNum(val) ? ("<c"+rAttr+sAttr+"><v>"+val+"</v></c>") : ("<c"+rAttr+sAttr+"/>"); std::string o; o.reserve(row.size()); o.append(row, 0, cs); o.append(rep); o.append(row, ce, row.size()-ce); return o; } // 替换文本占位符(内联,转义) static std::string EngTextTok(const std::string& row, const std::string& token, const std::string& val){ std::string o=row; std::string esc=EngEsc(val); size_t p; while((p=o.find(token))!=std::string::npos) o.replace(p, token.size(), esc); return o; } // 修正合并/筛选/条件格式里 > protoR 的行号(下移 shift),并把原型行范围扩到数据末尾 // 【v319】扩展 conditionalFormatting 的 sqref。用户的隔行底色是"偶数行加底色" // (MOD(ROW(),2) 之类表达式),模版里 sqref 只圈了原型行一行(如 A2:AQ2 / A10:AN10), // 克隆数据行后必须把 sqref 底部扩到全部数据行,表达式才会逐行判断、偶数行才上色。 // 规则:范围含原型行→底部扩到 lastData;整体在原型行下方→整体下移;支持多段(空格分隔)。 static std::string EngFixCondFormatSqref(std::string post, int protoR, int lastData){ int shift=lastData-protoR; std::regex sq_rx("sqref=\"([^\"]*)\""); std::regex twoRx("^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$"); std::regex oneRx("^([A-Z]+)(\\d+)$"); std::string out; auto beg=std::sregex_iterator(post.begin(),post.end(),sq_rx), end=std::sregex_iterator(); size_t last=0; for(auto it=beg; it!=end; ++it){ auto m=*it; size_t pos=(size_t)m.position(0), len=(size_t)m.length(0); out.append(post, last, pos-last); std::stringstream ss(m[1].str()); std::string rng, nv; bool first=true; while(ss>>rng){ if(!first) nv+=" "; first=false; std::smatch rm; if(std::regex_match(rng,rm,twoRx)){ int a=std::stoi(rm[2]), b=std::stoi(rm[4]); if(a<=protoR && b>=protoR) b=lastData; else { if(a>protoR)a+=shift; if(b>protoR)b+=shift; } nv+=rm[1].str()+std::to_string(a)+":"+rm[3].str()+std::to_string(b); } else if(std::regex_match(rng,rm,oneRx)){ int a=std::stoi(rm[2]); if(a==protoR) nv+=rm[1].str()+std::to_string(protoR)+":"+rm[1].str()+std::to_string(lastData); else { if(a>protoR)a+=shift; nv+=rm[1].str()+std::to_string(a); } } else nv+=rng; } out+="sqref=\""+nv+"\""; last=pos+len; } out.append(post, last, post.size()-last); return out; } static std::string EngFixRanges(std::string post, int protoR, int N){ int shift=N-1; int lastData=protoR+N-1; // 先扩条件格式 sqref(隔行底色范围)--独立处理,后面的 ref= 通用逻辑用 \bref 排除 sqref post=EngFixCondFormatSqref(post, protoR, lastData); // mergeCells: 每个 ref="X{r1}:Y{r2}",若 r>protoR 则 +shift。用 \bref 避免误匹配 sqref。 std::regex mrx("\\bref=\"([A-Z]+)(\\d+):([A-Z]+)(\\d+)\""); post=std::regex_replace(post, mrx, "\x01$1\x02$2\x03$3\x04$4\x05"); // 占位保护,手工处理 // 手工遍历替换 std::string out; size_t i=0; while(i<post.size()){ if(post[i]=='\x01'){ size_t e=post.find('\x05',i); std::string tok=post.substr(i,e-i+1); std::smatch m; std::regex rr("\x01([A-Z]+)\x02(\\d+)\x03([A-Z]+)\x04(\\d+)\x05"); std::regex_search(tok,m,rr); int r1=std::stoi(m[2]), r2=std::stoi(m[4]); if(r1>protoR) r1+=shift; if(r2>protoR) r2+=shift; out+="ref=\""+m[1].str()+std::to_string(r1)+":"+m[3].str()+std::to_string(r2)+"\""; i=e+1; } else out+=post[i++]; } // autoFilter ref="A9:AN10" -> 末行改为 lastData std::smatch am; std::regex arx("<autoFilter ref=\"([A-Z]+)(\\d+):([A-Z]+)(\\d+)\""); if(std::regex_search(out,am,arx)){ std::string rep="<autoFilter ref=\""+am[1].str()+am[2].str()+":"+am[3].str()+std::to_string(lastData)+"\""; out=std::regex_replace(out, arx, rep); } return out; } // 主函数:tplSheetXml=解压后的模版sheet1.xml; recordProvider 通过回调逐行给字段 // 返回生成好的完整 sheet1.xml。数据行在内存拼(N不极端时OK);如需极大N可改流式。 struct EngResult{ std::string sheet; bool ok; std::string err; }; static EngResult EngGenerate(const std::string& tplSheet, const std::vector<std::vector<std::string>>& records, const std::vector<std::string>& header, // 22 const std::vector<std::string>& totals){ // 6: person,date,4 sums EngResult R; R.ok=false; std::string pre,post; std::vector<std::string> rows; EngSplit(tplSheet, pre, rows, post); // 找原型行(含 | D)
int pi=-1;
for(size_t k=0;k<rows.size();k++) if(rows[k].find("[[D")!=std::string::npos){ pi=(int)k; break; }
if(pi<0){ R.err="prototype row ([[D.. | ) not found"; return R; } int protoR=EngRowNum(rows[pi]); int N=(int)records.size(); // 头部行(pi之前):替换 std::string body; for(int k=0;k<pi;k++){ std::string r=rows[k]; for(int h=0;h<(int)header.size();h++) r=EngTextTok(r, "", header[h]); body+=r; } // 数据行:克隆原型,逐条替换 const std::string& proto=rows[pi]; for(int i=0;i<N;i++){ std::string r=EngRenumber(proto, protoR, protoR+i); const auto& f=records[i]; // 数字列先做单元格改写 for(int k : ENG_NUMERIC){ std::string v = (k<(int)f.size())? f[k] : ""; r=EngNumCell(r, "", v); } // 其余文本列(总列数40→43:新增 指令单号[40]/上机日期[41]/机器编号名称[42] 均为文本) for(int k=0;k<43;k++){ if(ENG_NUMERIC.count(k)) continue; std::string v=(k<(int)f.size())? f[k] : ""; r=EngTextTok(r, "", v); } body+=r; } // 合计/尾部行(pi之后):下移 + 替换 // for(int k=pi+1;k<(int)rows.size();k++){ int rn=EngRowNum(rows[k]); std::string r=EngRenumber(rows[k], rn, rn+(N-1)); // 数字合计 for(int s=0;s<4;s++){ std::string v=(s+2<(int)totals.size())? totals[s+2] : ""; r=EngNumCell(r, "", v); } r=EngTextTok(r, "", totals.size()>0?totals[0]:""); r=EngTextTok(r, "", totals.size()>1?totals[1]:""); body+=r; } // 修 pre: dimension、pane 冻结 int lastRow = (rows.size()>(size_t)(pi+1)) ? (EngRowNum(rows.back())+(N-1)) : (protoR+N-1); { std::smatch dm; if(std::regex_search(pre, dm, std::regex("<dimension ref=\"([A-Z]+\\d+):([A-Z]+)\\d+\"/>"))){ std::string rep = "<dimension ref=\"" + dm[1].str() + ":" + dm[2].str() + std::to_string(lastRow) + "\"/>"; pre = std::regex_replace(pre, std::regex("<dimension ref=\"[A-Z]+\\d+:[A-Z]+\\d+\"/>"), rep); } } // 冻结:ySplit=protoR-1, topLeftCell=A{protoR} pre=std::regex_replace(pre, std::regex("<pane[^>]*/>"), "<pane ySplit=\""+std::to_string(protoR-1)+"\" topLeftCell=\"A"+std::to_string(protoR)+"\" activePane=\"bottomLeft\" state=\"frozenSplit\"/>"); // 修 post: 合并/筛选范围 post=EngFixRanges(post, protoR, N); R.sheet = pre + body + post; R.ok=true; return R; } // ===== 运行时读模版:解压单个条目 + 模版驱动克隆 ===== static bool InflateEntry(const std::vector<unsigned char>& b, const ZipEntryInfo& e, std::string& out, std::string& err){ size_t start=e.localHeaderOffset; uint16_t fn=0,ex=0; if(!ReadU16(b,start+26,fn)||!ReadU16(b,start+28,ex)){ err="bad LFH "+e.name; return false; } size_t dataOff=start+30+fn+ex; if(dataOff+e.compSize>b.size()){ err="entry data OOB "+e.name; return false; } if(e.method==0){ out.assign((const char*)&b[dataOff], e.compSize); return true; } std::vector<unsigned char> o; if(!InflateRaw(&b[dataOff], e.compSize, o)){ err="inflate failed "+e.name; return false; } out.assign((const char*)o.data(), o.size()); return true; } static std::string RemoveCalcChainRefs(std::string s){ s=std::regex_replace(s, std::regex("<Override[^>]*calcChain[^>]*/>"), ""); s=std::regex_replace(s, std::regex("<Relationship[^>]*calcChain[^>]*/>"), ""); return s; } // 【v319】解析 sharedStrings.xml → 每个 <si> 的拼接文本(供把占位符共享串转回内联)。 // 只在加载模版时跑一次(非每行热点),用 std::regex 无妨。 static std::vector<std::string> EngParseSST(const std::string& sst){ std::vector<std::string> v; // 【v319修正】std::regex 的 . 不匹配换行,而 Excel 的 sharedStrings.xml 里有些 <si> 跨行, // 若用 .*? 会漏掉跨行条目→索引整体错位→占位符张冠李戴。改用 [\s\S]*? 等效 DOTALL。 std::regex si_rx("<si\\b[^>]*>([\\s\\S]*?)</si>", std::regex::icase); std::regex t_rx("<t\\b[^>]*>([\\s\\S]*?)</t>", std::regex::icase); for(auto it=std::sregex_iterator(sst.begin(),sst.end(),si_rx); it!=std::sregex_iterator(); ++it){ std::string inner=(*it)[1].str(), txt; for(auto jt=std::sregex_iterator(inner.begin(),inner.end(),t_rx); jt!=std::sregex_iterator(); ++jt) txt += (*jt)[1].str(); v.push_back(txt); } return v; } // 【v319】把 sheet1.xml 里引用"含 | 的共享串"的单元格 <c ...t="s"...><v>N</v></c> 就地转回内联。
// 背景:用户在 Excel 里编辑模版(加底色/条件格式/样式)保存后,Excel 会把 [[占位符 | 从内联字符串 // 转成共享字符串,sheet1.xml 里就没有 | 文本了,引擎找不到原型行而报错(见规范 10.9)。这里在加载
// 时把占位符单元格转回内联,用户就能自由用 Excel 改模版。非占位符共享串(公司名/列标题等)保持不动,
// sharedStrings.xml 仍原样保留在输出里,那些单元格照常引用它。
static std::string EngInlineSharedPlaceholders(const std::string& sheet, const std::vector<std::string>& sst){
if(sheet.find("t=\"s\"")==std::string::npos) return sheet; // 无共享串单元格,免处理
std::string out; out.reserve(sheet.size()+256);
std::regex c_rx("<c\\b([^>]*?)\\bt=\"s\"([^>]*?)>\\s*<v>(\\d+)</v>\\s*</c>", std::regex::icase);
auto beg=std::sregex_iterator(sheet.begin(),sheet.end(),c_rx), end=std::sregex_iterator();
size_t last=0;
for(auto it=beg; it!=end; ++it){
auto m=*it; size_t pos=(size_t)m.position(0), len=(size_t)m.length(0);
out.append(sheet, last, pos-last);
int idx=std::atoi(m[3].str().c_str());
if(idx>=0 && idx<(int)sst.size() && sst[idx].find("[[")!=std::string::npos)
out += "<c"+m[1].str()+m[2].str()+" t=\"inlineStr\"><is><t xml:space=\"preserve\">"+sst[idx]+"</t></is></c>";
else
out.append(sheet, pos, len);
last=pos+len;
}
out.append(sheet, last, sheet.size()-last);
return out;
}
static bool CloneZipTemplate(const std::string& tplPath, const std::string& outPath,
const std::vector<std::vector<std::string>>& records,
const std::vector<std::string>& header, const std::vector<std::string>& totals,
std::string& err){
std::vector<unsigned char> tpl; if(!ReadWholeFile(tplPath,tpl,err)) return false;
std::vector<ZipEntryInfo> entries;
if(!ReadZipEntries(tpl,entries,err)) return false;
const ZipEntryInfo* sheetE=nullptr;
for(auto&e:entries) if(e.name=="xl/worksheets/sheet1.xml"){ sheetE=&e; break; }
if(!sheetE){ err="template missing sheet1.xml"; return false; }
std::string tplSheet;
if(!InflateEntry(tpl,*sheetE,tplSheet,err)) return false;
// 【v319】占位符若被 Excel 转成共享字符串,这里解析 sharedStrings.xml 就地转回内联,
// 再走原引擎。这样用户可自由用 Excel 编辑模版(加底色/条件格式/样式)保存。
{
const ZipEntryInfo* sstE=nullptr;
for(auto&e:entries) if(e.name=="xl/sharedStrings.xml"){ sstE=&e; break; }
if(sstE){
std::string sstXml, e2;
if(InflateEntry(tpl,*sstE,sstXml,e2))
tplSheet=EngInlineSharedPlaceholders(tplSheet, EngParseSST(sstXml));
}
}
EngResult R=EngGenerate(tplSheet, records, header, totals);
if(!R.ok){ err="engine: "+R.err; return false; }
const std::string newSheet=R.sheet;
uint16_t smt=sheetE->modTime, smd=sheetE->modDate;
std::ofstream out; OpenOut(out,outPath,std::ios::trunc);
if(!out){ err="cannot create output "+outPath; return false; }
struct WE{ std::string name; uint16_t method,modTime,modDate; uint32_t crc,compSize,uncompSize,offset; };
std::vector<WE> written; written.reserve(entries.size()+1);
for(auto&e:entries){
if(e.name=="xl/worksheets/sheet1.xml") continue;
if(e.name=="xl/calcChain.xml") continue;
if(e.name=="[Content_Types].xml" || e.name=="xl/_rels/workbook.xml.rels"){
std::string content;
if(!InflateEntry(tpl,e,content,err)) return false;
content=RemoveCalcChainRefs(content);
uint32_t c=Crc32(content); uint32_t s=(uint32_t)content.size();
uint32_t off=(uint32_t)out.tellp();
WriteLocalHeaderAndData(out, e.name, 0, e.modTime, e.modDate,c, s, s, content); written.push_back({e.name,0,e.modTime,e.modDate,c,s,s,off});
continue;
}
size_t ss,sl;
if(!GetLocalEntrySpan(tpl,e,ss,sl,err)) return false;
uint32_t off=(uint32_t)out.tellp();
out.write((const char*)&tpl[ss],(std::streamsize)sl);
written.push_back({e.name,e.method,e.modTime,e.modDate,e.crc32,e.compSize,e.uncompSize,off});
}
{ uint32_t c=Crc32(newSheet); uint32_t s=(uint32_t)newSheet.size();
uint32_t off=(uint32_t)out.tellp();
WriteLocalHeaderAndData(out,"xl/worksheets/sheet1.xml",0,smt,smd,c,s,s,newSheet);
written.push_back({"xl/worksheets/sheet1.xml",0,smt,smd,c,s,s,off});
}
uint32_t cdStart=(uint32_t)out.tellp();
for(auto&w:written){
WriteU32(out,0x02014b50u);WriteU16(out,20);WriteU16(out,20);WriteU16(out,0);WriteU16(out,w.method);
WriteU16(out,w.modTime);WriteU16(out,w.modDate);WriteU32(out,w.crc);WriteU32(out,w.compSize);WriteU32(out,w.uncompSize);
WriteU16(out,(uint16_t)w.name.size());WriteU16(out,0);WriteU16(out,0);WriteU16(out,0);WriteU16(out,0);
WriteU32(out,0);WriteU32(out,w.offset);out.write(w.name.data(),(std::streamsize)w.name.size());
}
uint32_t cdSize=(uint32_t)((uint32_t)out.tellp()-cdStart);
WriteU32(out,0x06054b50u);WriteU16(out,0);WriteU16(out,0);WriteU16(out,(uint16_t)written.size());
WriteU16(out,(uint16_t)written.size());WriteU32(out,cdSize);WriteU32(out,cdStart);WriteU16(out,0);
out.close();
return true;
}
// ====================================================================
// 9. 平台相关的外壳:Windows下是真正的DLL导出函数(脚本引擎通过
// call_user_dll调用),负责把中间文件按系统代码页(GBK)读出来转成
// UTF-8交给上面这些平台无关的核心逻辑处理;非Windows下提供一个
// main()测试入口,用于本地验证核心逻辑(zip克隆/xml拼装)是否正确,
// 不改变上面任何一行核心逻辑,两边跑的是同一份代码
// ====================================================================#ifdef _WIN32
// 脚本引擎的file_write()/call_user_dll参数传递实际是按系统本地代码页
// (中文Windows是GBK)读写字符串的,不是UTF-8--这里统一转成UTF-8,后面
// 所有解析/拼xml都按UTF-8字节串处理
static std::string AnsiBytesToUtf8(const std::string& ansi)
{
if (ansi.empty()) return "";
int wlen = MultiByteToWideChar(CP_ACP, 0, ansi.c_str(), (int)ansi.size(), NULL, 0);
std::wstring w(wlen, 0);
MultiByteToWideChar(CP_ACP, 0, ansi.c_str(), (int)ansi.size(), &w[0], wlen);
int ulen = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), NULL, 0, NULL, NULL);
std::string u(ulen, 0);
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), &u[0], ulen, NULL, NULL);
return u;
}
// ====================================================================
// V13(直连版):DLL 自己用 ODBC 连 SQL Server、跑查询、原生逐行取数(宽字符,
// 中文越南文不丢),复用上面的流式 xlsx 引擎写带模板的文件。彻底去掉 4GL 脚本
// 逐行 db_res + 临时文件往返(那是现行5分钟的瓶颈)。
// ====================================================================
// UTF-16 码元 -> UTF-8(手写,处理代理对)
static std::string Utf16ToUtf8Units(const unsigned short* u, size_t n)
{
std::string out; out.reserve(n);
for (size_t i = 0; i < n; ) {
unsigned int c = u[i++];
unsigned int cp = c;
if (c >= 0xD800 && c <= 0xDBFF && i < n) {
unsigned int c2 = u[i];
if (c2 >= 0xDC00 && c2 <= 0xDFFF) { cp = 0x10000 + ((c - 0xD800) << 10) + (c2 - 0xDC00); i++; }
}
if (cp < 0x80) out.push_back((char)cp);
else if (cp < 0x800) { out.push_back((char)(0xC0|(cp>>6))); out.push_back((char)(0x80|(cp&0x3F))); }
else if (cp < 0x10000) { out.push_back((char)(0xE0|(cp>>12))); out.push_back((char)(0x80|((cp>>6)&0x3F))); out.push_back((char)(0x80|(cp&0x3F))); }
else { out.push_back((char)(0xF0|(cp>>18))); out.push_back((char)(0x80|((cp>>12)&0x3F))); out.push_back((char)(0x80|((cp>>6)&0x3F))); out.push_back((char)(0x80|(cp&0x3F))); }
}
return out;
}
// 用宽字符(SQL_C_WCHAR)取一列 -> UTF-8;超长字段循环取全
static std::string OdbcGetColUtf8(SQLHSTMT stmt, SQLUSMALLINT col)
{
std::vector<unsigned short> units;
SQLWCHAR buf[2048];
SQLLEN ind = 0;
for (;;) {
SQLRETURN r = SQLGetData(stmt, col, SQL_C_WCHAR, buf, (SQLLEN)sizeof(buf), &ind);
if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) break;
if (ind == SQL_NULL_DATA) break;
size_t capUnits = sizeof(buf) / sizeof(SQLWCHAR) - 1;
size_t thisUnits;
if (ind == SQL_NO_TOTAL) thisUnits = capUnits;
else {
size_t remUnits = (size_t)ind / sizeof(SQLWCHAR);
thisUnits = (remUnits > capUnits) ? capUnits : remUnits;
}
for (size_t k = 0; k < thisUnits; k++) units.push_back((unsigned short)buf[k]);
if (r == SQL_SUCCESS) break;
}
return Utf16ToUtf8Units(units.data(), units.size());
}
static void StripNlInplace(std::string& s)
{
for (size_t i = 0; i < s.size(); i++) { char c = s[i]; if (c=='\r'||c=='\n'||c=='\t') s[i]=' '; }
}
static std::string FmtNum(double v)
{
char b[64];
snprintf(b, sizeof(b), "%.4f", v);
std::string s(b);
if (s.find('.') != std::string::npos) {
size_t e = s.size();
while (e > 0 && s[e-1] == '0') e--;
if (e > 0 && s[e-1] == '.') e--;
s = s.substr(0, e);
}
if (s.empty()) s = "0";
return s;
}
// ODBC 诊断:驱动返回的是 ANSI(GBK)报错;转 UTF-8 交给 SafeCopyRet 再转回 GBK 正确显示
static std::string OdbcDiagV(SQLSMALLINT type, SQLHANDLE h)
{
SQLCHAR state[7], msg[512];
SQLINTEGER native; SQLSMALLINT len;
std::string out;
for (SQLSMALLINT i = 1; i <= 3; i++) {
SQLRETURN r = SQLGetDiagRec(type, h, i, state, &native, msg, sizeof(msg), &len);
if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) break;
if (!out.empty()) out += " || ";
out += std::string((char*)state) + " " + std::string((char*)msg);
}
if (out.empty()) out = "(no diag)";
return AnsiBytesToUtf8(out);
}
// 解析配置文件文本:找出连接串(含 SERVER= 的那行)和版本(VERSION= 那行)
// 解析配置:收集所有含 SERVER= 的连接串到 conns(按文件顺序:第1条=主内网,
// 第2条=备外网……),VERSION= 取版本号。DLL 连接时按此顺序依次尝试,第一条连上即用。
static void ParseConfig(const std::string& text, std::vector<std::string>& conns, std::string& ver)
{
conns.clear(); ver.clear();
std::vector<std::string> lines = SplitLines(text);
for (size_t i = 0; i < lines.size(); i++) {
std::string l = lines[i];
while (!l.empty() && (l.back()=='\r'||l.back()=='\n'||l.back()==' '||l.back()=='\t')) l.pop_back();
size_t s = 0; while (s < l.size() && (l[s]==' '||l[s]=='\t')) s++;
l = l.substr(s);
if (l.empty()) continue;
if (l[0]=='#' || (l.size()>=2 && l[0]=='/' && l[1]=='/')) continue; // 允许 # 或 // 注释行
std::string up = l; for (size_t k = 0; k < up.size(); k++) up[k] = (char)toupper((unsigned char)up[k]);
if (up.rfind("VERSION=", 0) == 0) { ver = l.substr(8); }
else if (up.find("SERVER=") != std::string::npos) { conns.push_back(l); }
}
}
// 供脚本查本地配置文件版本(para=配置文件路径, ACP)
extern "C" __declspec(dllexport) int get_config_version(char* para, char* ret)
{
SafeCopyRet(ret, 500, "");
if (!para) return 0;
std::string cfgPath = AnsiBytesToUtf8(std::string(para));
std::vector<unsigned char> cb; std::string e;
if (!ReadWholeFile(cfgPath, cb, e)) return 0;
std::string text((const char*)cb.data(), cb.size());
std::vector<std::string> conns; std::string ver;
ParseConfig(text, conns, ver);
SafeCopyRet(ret, 500, ver);
return 1;
}
// 参数文件(脚本用 file_write 写, GBK):
// 行0=连接配置ini路径 行1=模板路径 行2=输出xlsx路径
// 行3=查询人 行4=查询日期 行5=22个条件回显(用|~|拼接) 行6=SQL(可跨多行)
extern "C" __declspec(dllexport) int export_direct(char* para, char* ret)
{
SafeCopyRet(ret, 500, "");
if (!para) { SafeCopyRet(ret, 500, "no param"); return 0; }
DWORD t0 = GetTickCount();
std::ifstream pf; pf.open(AnsiToWide(std::string(para)).c_str(), std::ios::binary);
if (!pf) { SafeCopyRet(ret, 500, "cannot open param file"); return 0; }
std::stringstream pss; pss << pf.rdbuf();
std::string ptxt = AnsiBytesToUtf8(pss.str());
// 参数分隔:优先用唯一分隔符 |~~PARAM~~|(字段里即使含换行也不会错位);
// 找不到分隔符则回退按行切(兼容旧版报表A)。
std::vector<std::string> L;
{
static const std::string PD = "|~~PARAM~~|";
if (ptxt.find(PD) != std::string::npos) {
size_t p = 0, f;
while ((f = ptxt.find(PD, p)) != std::string::npos) { L.push_back(ptxt.substr(p, f - p)); p = f + PD.size(); }
L.push_back(ptxt.substr(p));
} else {
L = SplitLines(ptxt);
}
}
if (L.size() < 9) { SafeCopyRet(ret, 500, "param fields<9 got=" + std::to_string((int)L.size())); return 0; }
auto rtrim = [](std::string s){ while(!s.empty() && (s.back()=='\r'||s.back()=='\n'||s.back()==' '||s.back()=='\t')) s.pop_back(); return s; };
std::string cfgPath = rtrim(L[0]);
std::string tplPath = rtrim(L[1]);
std::string outPath = rtrim(L[2]);
std::string qPerson = L[3];
std::string qDate = L[4];
std::string header22 = L[5];
std::string plainTplPath = rtrim(L[6]);
std::string plainOutPath = rtrim(L[7]);
std::string sqlUtf8 = L[8];
for (size_t i = 9; i < L.size(); i++) { sqlUtf8 += "\n"; sqlUtf8 += L[i]; }
std::string err;
std::vector<unsigned char> cbytes;
if (!ReadWholeFile(cfgPath, cbytes, err)) { SafeCopyRet(ret, 500, "cannot read config: " + cfgPath); return 0; } std::string ctext((const char*)cbytes.data(), cbytes.size()); std::vector<std::string> conns; std::string cver;
ParseConfig(ctext, conns, cver);
if (conns.empty()) { SafeCopyRet(ret, 500, "config no connection line (SERVER=)"); return 0; }
SQLHENV env = NULL; SQLHDBC dbc = NULL; SQLHSTMT stmt = NULL; SQLRETURN r;
SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
SQLCHAR outc[1024]; SQLSMALLINT outl = 0;
// 依次尝试配置里的连接串(第1条=主内网,第2条=备外网……),第一条连上即用;
// 全部失败才报错,并把每条的失败原因都带回来便于排查。每条登录超时8秒,主连不上不久等。
bool connected = false; std::string lastErr;
for (size_t ci = 0; ci < conns.size(); ci++) {
SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
SQLSetConnectAttr(dbc, SQL_ATTR_LOGIN_TIMEOUT, (SQLPOINTER)(SQLLEN)8, 0);
r = SQLDriverConnect(dbc, NULL, (SQLCHAR*)conns[ci].c_str(), SQL_NTS, outc, sizeof(outc), &outl, SQL_DRIVER_NOPROMPT);
if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { connected = true; break; }
if (!lastErr.empty()) lastErr += " ; ";
lastErr += "[conn" + std::to_string((int)ci + 1) + "] " + OdbcDiagV(SQL_HANDLE_DBC, dbc);
SQLFreeHandle(SQL_HANDLE_DBC, dbc); dbc = NULL;
}
if (!connected) {
SafeCopyRet(ret, 500, "CONNECT FAILED (tried " + std::to_string((int)conns.size()) + "): " + lastErr);
SQLFreeHandle(SQL_HANDLE_ENV, env); return 0;
}
SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
SQLSetStmtAttr(stmt, SQL_ATTR_QUERY_TIMEOUT, (SQLPOINTER)(SQLLEN)600, 0);
std::string sqlAnsi = Utf8ToAnsi(sqlUtf8);
// 【性能修复 v315】多语句+临时表批处理会让老 {SQL Server} 驱动退化成"服务器游标",
// 逐行取数每行一次网络往返(内网约81秒 / 经VPN约115秒,3.9万行)。这里在
// /*__EXECSPLIT__*/ 标记处把批处理拆成两段:先执行"建临时表"部分(无结果集),抽干
// 后再单独执行"最终SELECT"--单条SELECT驱动走firehose 整包传输(几秒),不再逐行往返。
// 临时表(#jebase/#jeagg/#base)是连接级作用域,同一连接上不会丢。无标记时按原单次
// 执行(向后兼容:旧脚本/其它报表不受影响,标记本身是SQL注释即使被当整段执行也无害)。
const std::string EXECSPLIT = "/*__EXECSPLIT__*/";
size_t splitPos = sqlAnsi.find(EXECSPLIT);
if (splitPos != std::string::npos) {
std::string setupSql = sqlAnsi.substr(0, splitPos);
std::string selSql = sqlAnsi.substr(splitPos + EXECSPLIT.size());
r = SQLExecDirect(stmt, (SQLCHAR*)setupSql.c_str(), SQL_NTS);
if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) {
SafeCopyRet(ret, 500, "QUERY FAILED (setup): " + OdbcDiagV(SQL_HANDLE_STMT, stmt));
SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc);
SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); return 0;
}
// 抽干建表批处理的所有(NOCOUNT下无列的)结果集,确保 #base 等临时表全部建好
{ int g = 0; SQLRETURN mr;
do { mr = SQLMoreResults(stmt); g++; }
while (mr != SQL_NO_DATA && (mr == SQL_SUCCESS || mr == SQL_SUCCESS_WITH_INFO) && g < 100); }
SQLFreeStmt(stmt, SQL_CLOSE);
// 最终SELECT单条执行 → firehose整包取数
r = SQLExecDirect(stmt, (SQLCHAR*)selSql.c_str(), SQL_NTS);
} else {
r = SQLExecDirect(stmt, (SQLCHAR*)sqlAnsi.c_str(), SQL_NTS);
}
if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) {
// 诊断兜底:把解析到的各字段+完整SQL写到 outPath+".paramdbg.txt",方便远程排错
{
std::string dbg = "fields=" + std::to_string((int)L.size())
+ "\nqPerson=[" + qPerson + "]\nqDate=[" + qDate + "]\nheader22=[" + header22 + "]"
+ "\nplainTpl=[" + plainTplPath + "]\nplainOut=[" + plainOutPath + "]"
+ "\n--- SQL ---\n" + sqlUtf8;
std::ofstream df; OpenOut(df, outPath + ".paramdbg.txt", std::ios::trunc);if (df) { df << dbg; df.close(); }}
SafeCopyRet(ret, 500, "QUERY FAILED: "+ OdbcDiagV(SQL_HANDLE_STMT, stmt));
SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc);
SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); return 0;
}
// 支持"SET NOCOUNT ON + 多语句批处理":脚本会先建 #jebase 临时表(SELECT INTO/
// CREATE INDEX 等前置语句在 NOCOUNT ON 下无结果集),这里跳过前面无列的结果集,
// 定位到第一个有列的结果集(即主查询)。guard 防死循环。
SQLSMALLINT ncols = 0; SQLNumResultCols(stmt, &ncols);
{
int guard = 0;
while (ncols == 0 && guard < 50) {
SQLRETURN mr = SQLMoreResults(stmt);
if (mr == SQL_NO_DATA) break;
if (mr != SQL_SUCCESS && mr != SQL_SUCCESS_WITH_INFO) break;
SQLNumResultCols(stmt, &ncols);
guard++;
}
}
if (ncols != 43) {
SafeCopyRet(ret, 500, "expected 43 cols got " + std::to_string((int)ncols));
SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc);
SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); return 0;
}
std::string rowsPath = RowsPath(outPath);
std::ofstream rf; OpenOut(rf, rowsPath, std::ios::trunc);
if (!rf) {
SafeCopyRet(ret, 500, "cannot create rows temp");
SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc);
SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); return 0;
}
int N = 0;
double s_inp = 0, s_ship = 0, s_wait = 0, s_ass = 0;
std::string rowline; rowline.reserve(4096);
while ((r = SQLFetch(stmt)) == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) {
rowline.clear();
for (SQLUSMALLINT c = 1; c <= 43; c++){
std::string f = OdbcGetColUtf8(stmt, c);
StripNlInplace(f);
if (c > 1) rowline += FIELD_DELIM; rowline += f;
// 合计列位固定(ODBC 1基):34订单数量,35已出货,36待出货,40生产数量Y。
// 新增3列(指令单号/上机日期/机器编号名称)追加在41~43,不影响这些合计列位。
if (c == 34) s_inp += atof(f.c_str());
else if (c == 35) s_ship += atof(f.c_str());
else if (c == 36) s_wait += atof(f.c_str());
else if (c == 40) s_ass += atof(f.c_str());
}
rf << rowline << "\n";
N++;
}
rf.close();
SQLFreeHandle(SQL_HANDLE_STMT, stmt);
SQLDisconnect(dbc);
SQLFreeHandle(SQL_HANDLE_DBC, dbc);
SQLFreeHandle(SQL_HANDLE_ENV, env);
// 【诊断 v316】取数阶段(连接+执行SQL+逐行SQLFetch取回)累计耗时,与后面
// "生成xlsx"阶段分开,一眼看出 116 秒到底花在取数(网络/游标)还是生成(正则引擎)。
DWORD ms_fetch = GetTickCount() - t0;
// 【防呆 v318】明细行为 0 → 不生成任何 xlsx,直接返回 NODATA,由脚本弹"无数据"提示
// 并终止(不 shell 打开空文件、不留空文档)。消息以 "export_direct NODATA" 开头,
// 脚本按前 20 字符识别这一分支(与成功分支 "export_direct OK " 前 17 字符互不冲突)。
if (N == 0) {
RemovePath(rowsPath);
SafeCopyRet(ret, 500, "export_direct NODATA rows=0 fetch_ms="
+ std::to_string((long)ms_fetch) + " v=" + std::string(DLL_VERSION));
return 1;
}
std::vector<std::string> header = SplitUS(header22);
while (header.size() < 22) header.push_back("");
std::vector<std::string> totals;
totals.push_back(qPerson);
totals.push_back(qDate);
totals.push_back(FmtNum(s_inp));
totals.push_back(FmtNum(s_ship));
totals.push_back(FmtNum(s_wait));
totals.push_back(FmtNum(s_ass));
// 读 rows.tmp 到内存(SQL列序,US分隔),交模版引擎按占位符填充
std::vector<std::vector<std::string>> records;
{
std::ifstream rf; OpenIn(rf, rowsPath);
if(!rf){ RemovePath(rowsPath); SafeCopyRet(ret,500,"rows temp missing"); return 0; }
std::string line;
while(std::getline(rf,line)){
if(!line.empty()&&line.back()=='\r') line.pop_back();
if(line.empty()) continue;
records.push_back(SplitUS(line));
}
}
// 打印版(主表):主模版(带占位符),回显header + 合计totals。
// 仅当传入了打印版模版和输出路径时才生成--脚本侧"否(只导出查询版)"会把这两个字段置空以跳过。
int genFull = 0, genPlain = 0;
if(!tplPath.empty() && !outPath.empty()){
if(!CloneZipTemplate(tplPath, outPath, records, header, totals, err)){
RemovePath(rowsPath);
SafeCopyRet(ret,500,"write xlsx failed(main): "+err); return 0;
}
genFull = 1;
}
// 查询版(纯净表):纯净模版(带占位符,无[[H | )。同样按路径是否为空判断。 if(!plainTplPath.empty() && !plainOutPath.empty()){ if(!CloneZipTemplate(plainTplPath, plainOutPath, records, header, totals, err)){ RemovePath(rowsPath); SafeCopyRet(ret,500,"write xlsx failed(plain): "+err); return 0; } genPlain = 1; } RemovePath(rowsPath); DWORD ms = GetTickCount() - t0; // 消息保持以 "export_direct OK rows=" 开头(脚本按前17字符判成功),后面追加 // fetch_ms/gen_ms/版本号:fetch_ms=取数,gen_ms=生成xlsx,v=当前DLL版本(确认加载)。 SafeCopyRet(ret, 500, "export_direct OK rows=" + std::to_string(N) + " elapsed_ms=" + std::to_string((long)ms) + " fetch_ms=" + std::to_string((long)ms_fetch) + " gen_ms=" + std::to_string((long)(ms - ms_fetch)) + " v=" + std::string(DLL_VERSION) + " full=" + std::to_string(genFull) + " plain=" + std::to_string(genPlain)); return 1; } extern "C" __declspec(dllexport) int get_dll_version(char* para, char* ret) { SafeCopyRet(ret, 500, DLL_VERSION); return 1; } extern "C" __declspec(dllexport) int export_customer_order_ship_detail_excel(char* para, char* ret) { SafeCopyRet(ret, 500, ""); if (!para) { SafeCopyRet(ret, 500, "no param"); return 0; } // para是脚本引擎直传的中间文件路径,按系统ACP(GBK)字节串传来, // 转成宽字符再按宽路径打开,兼容中间文件放在中文/Unicode目录下的情况 std::ifstream f; f.open(AnsiToWide(std::string(para)).c_str(), std::ios::binary); if (!f) { SafeCopyRet(ret, 500, "cannot open intermediate file"); return 0; } std::stringstream ss; ss << f.rdbuf(); std::string utf8Text = AnsiBytesToUtf8(ss.str()); ExportJob job; std::string err; if (!LoadJob(utf8Text, job, err)) { SafeCopyRet(ret, 500, "parse fail: " + err); return 0; } if (job.mode == "INIT") return DoInit(job, ret); if (job.mode == "APPEND") return DoAppend(job, ret); if (job.mode == "FINAL") return DoFinal(job, ret); SafeCopyRet(ret, 500, "unknown mode"); return 0; } #else // ---- 非Windows测试入口:直接构造INIT/APPEND/FINAL三个ExportJob并 // 依次调用,拿真实模版文件生成一份.xlsx,用来验证核心逻辑,不代表 // 最终交付形态(交付的是上面Windows分支那个DLL) ---- #include <iostream> int main(int argc, char** argv) { if (argc < 3) { std::cerr << "usage: " << argv[0] << " <template.xlsx> <output.xlsx>\n"; return 1; } std::string tpl = argv[1]; std::string out = argv[2]; char ret[500]; // ---- 直连双输出测试:argc>=5 时,构造 rows.tmp(SQL列序),测主表+纯净表 ---- if (argc >= 5) { std::string fullTpl=argv[1], fullOut=argv[2], plainTpl=argv[3], plainOut=argv[4]; int Nd=25; std::vector<std::vector<std::string>> records; for(int i=0;i<Nd;i++){ std::vector<std::string> r(43); for(int c=0;c<43;c++) r[c]="F"+std::to_string(c); r[0]=std::to_string(i+1); r[2]="2026-08-11"; r[3]="Nguyen Thi 孙浩源"; r[14]="2026/8/10"; r[24]="Phat trien & <x> 开发"; r[33]=std::to_string(40+i); r[34]=std::to_string(i); r[35]="40"; r[36]="PRS"; r[37]="KHBZ"+std::to_string(i);r[38]="BZ"+std::to_string(i); r[39]=std::to_string(96+i); r[40]="ZL-260813-0"+std::to_string(i); r[41]="2026年08月13日"; r[42]="M001织机A"; records.push_back(r); } std::vector<std::string> header(22,""); header[0]="2026-08-01"; header[1]="2026-08-11"; header[2]="SL-016 测试客户"; std::vector<std::string> totals={"U0012345 张三","2026-08-11 14:23:05","1625","300","1325","2540"}; std::string err; bool okF=CloneZipTemplate(fullTpl, fullOut, records, header, totals, err); std::cout<<"FULL template -> "<<okF<<" : "<<err<<"\n"; bool okP=CloneZipTemplate(plainTpl, plainOut, records, header, totals, err); std::cout<<"PLAIN template -> "<<okP<<" : "<<err<<"\n"; return (okF && okP) ? 0 : 1; } int N = 25; // 测试行数 ExportJob initJob; initJob.mode = "INIT"; initJob.tplPath = tpl; initJob.outPath = out; initJob.totalCounts = N; initJob.header = { "2026-08-01", "2026-08-11", "SL-016 \xe6\x88\x90\xe9\x9a\x86\xe6\xb5\x8b\xe8\xaf\x95\xe5\xae\xa2\xe6\x88\xb7", "26081101600001", "SL-Q166", "D090085657", "GD-260811-018", "", "FJHS2605-067", "W CHALLENGER THREE8", "1181494-BBLC", "", "", "", "", "", "", "", "", "", "PRS", "A" }; int r1 = DoInit(initJob, ret); std::cout << "INIT -> " << r1 << " : " << ret << "\n"; if (!r1) return 1; ExportJob appendJob; appendJob.mode = "APPEND"; appendJob.outPath = out; appendJob.startOffset = 0; appendJob.chunkCount = N; for (int i = 0; i < N; i++) { std::vector<std::string> row; row.push_back(std::to_string(i + 1)); // 序号 row.push_back("BA-20260810-" + std::to_string(1000 + i)); // 订单单据编号 row.push_back("2026-08-11"); // 单据日期 row.push_back("Nguyễn Thị Thanh 孙浩源"); // 制单人(越南文+中文,验证Unicode往返) row.push_back("\xe5\xb7\xb2\xe5\xae\xa1\xe6\xa0\xb8"); // 单据状态 row.push_back("YA DUO \xe8\x8a\xbd\xe6\x9c\xb5"); // 审核人 row.push_back("SL-016"); // 客户编号 row.push_back("YA DUO \xe8\x8a\xbd\xe6\x9c\xb5"); // 业务员 row.push_back(" 26081101600001"); // 内部订号 row.push_back(i % 2 == 0 ? "\xe9\x87\x8f\xe4\xba\xa7" : "\xe9\x94\x80\xe6\xa0\xb7"); // 订单类型 row.push_back("T1"); // 生产部门 row.push_back("\xe9\x9e\x8b\xe5\xb8\xa6\xe3\x80\x8b\xe9\xab\x98\xe9\x80\x9f\xe9\x9e\x8b\xe5\xb8\xa6"); // 成品项次 row.push_back("GD-260811-018"); // 工单号 row.push_back(""); // 预告工单号 row.push_back("2026/8/10"); // 下单日期 row.push_back("2026-08-15"); // 交付日期 row.push_back("FJHS2605-067"); // 客户采购单号 row.push_back("1181494-BBLC"); // 客户型体 row.push_back(std::to_string(i + 1)); // 项次 row.push_back("W CHALLENGER THREE8"); // 客户指令 row.push_back(""); // 企划单号 row.push_back(""); // 企业单号 row.push_back("D090085657"); // 客户物料编号 row.push_back("SL-Q166"); // 内部料号 row.push_back("Phát triển phẩm mới & <special> 开发订单"); // 品名描述(越南文+中文+特殊字符&<) row.push_back("6mm"); // 规格 row.push_back("139-18-00 BLACK"); // 通用色号 row.push_back("BK70001/G70078/DXYH"); // 内部色号 row.push_back("43in"); // 长度 row.push_back(""); // 是否打确认样 row.push_back("\xe5\x90\xa6"); // 是否防虹吸 row.push_back("\xe5\x8d\xb0\xe5\x88\xb7\xe8\x83\xb6\xe7\x89\x87"); // 订购胶片 row.push_back(""); // 织带印刷 row.push_back(std::to_string(40 + i)); // 订单数量 row.push_back(std::to_string(i)); // 已出货数量 row.push_back(std::to_string(40)); // 待出货数量 row.push_back("PRS"); // 订单单位 row.push_back("\xe5\xae\xa2\xe4\xba\xba" "FOC\xe9\x87\x8f\xe4\xba\xa7\xe8\xaf\x95\xe5\x81\x9a"); // 客户订单备注 row.push_back(""); // 备注 row.push_back(std::to_string(96 + i)); // 生产数量(Y) // 模拟SQL端:整行拼成row_line后编码为UTF-16LE十六进制(纯ASCII), // 让DoAppend走真实的十六进制解码路径 std::string rowline = JoinUS(row); appendJob.rows.push_back({ Utf8ToHexUtf16Le(rowline) }); } int r2 = DoAppend(appendJob, ret); std::cout << "APPEND -> " << r2 << " : " << ret << "\n"; if (!r2) return 1; ExportJob finalJob; finalJob.mode = "FINAL"; finalJob.outPath = out; finalJob.totalCounts = N; finalJob.totals = { "U0012345 \xe5\xbc\xa0\xe4\xb8\x89", "2026-08-11 14:23:05", "1625", "300", "1325", "2540" }; int r3 = DoFinal(finalJob, ret); std::cout << "FINAL -> " << r3 << " : " << ret << "\n"; return r3 ? 0 : 1; } #endif |
|
附件(登录后才可下载):2608141258_25.cpp 说明: 原文件:customer_order_ship_detail_export_v15.cpp 大小:140946 Byte
|
于 2026-08-14 13:28 被 fwj3861 修改

官方认证第三方服务团队 (越南-叶子) QQ:121305301 TEL:+84869350626
承接越南北部-河内周边省份-OIT部署和维护服务(24小时上门或远程服务)
服务介绍:http://www.onlyit.cn/onlyit_service.html
|
|
fwj3861 [个人空间] QQ名 越南-叶子(121305301)
    注册  2010-09-09 发贴数 641 精华贴 10 原创贴 20 来自 越南 状态 正常
级别 贵宾 |
|
|
再发报表脚本文件》》》》》》 // 文件名:ba_cosd_report_export_only.txt 版本:V62-导出版【报表A】 // 用途:客户订单出货明细【打印版/查询版】导出。打开报表→(是否导出打印版确认)→ // 调 cosd_direct_export.dll 直连SQL取数、运行时读模版生成xlsx。不在网格显示明细。 // 关键约定(完整说明见《DLL编写规范》): // 1. func_def_field 列数须与 func_query 末尾 db_run 列数一致。 // 2. DLL版本联动:改DLL须同步 EXPECTED_DLL_VER 并上传文档管理替换正确文件。 // 3. 长SQL/文案行必须拆短(每行独立 sql+= "..."; 闭合),避免粘贴进ERP编辑器时 // 整行或尾部被截断,导致"引号没有匹配"解析错误;并控制脚本总体积。 // 4. 越南文文案用无声调ASCII或实测可显示的带声调版(GBK显示层限制)。 // 5. JE机台作业单3列(指令单号/上机日期/机器编号名称)按工单号gdh关联,追加在SQL // 末尾(索引40-42),查询版模版放 //。 // 历史版本与踩坑详见 cpp 目录各版本文件与《DLL编写规范》。 string change_obj; int obj_change() { return 1; }; // ========================================== // 辅助函数:去掉字符串里嵌入的换行符(用空格替换), // 用于导出DLL用的中间数据文件之前。备注/客户订单 // 备注这类用户手工填写的自由文本字段,可能被输入过 // 换行,不清理的话会被误判成中间文件的"下一条记录" // 分隔符,导致后面所有行错位(参照《DLL编写规范》第3节) // ========================================== string strip_nl(string s) { string r; r = str_replace(s, nl(), ' '); return r; }; // ========================================== // 辅助函数:文本型筛选框的"存在性"校验。提取数据前, // 勾选且填了值的文本框,先单独查一下这个内容在数据库 // 里到底存不存在,不掺其它筛选条件/日期范围一起判断 // (只是排查"是不是打错了/根本没这个数据",不是重复主查询)。 // 分两种来源:大部分字段来自ebs_vr_item_attr这张属性 // PIVOT表(按attr_id区分),少数字段是ebs_vr_item表上的 // 直接列 // ========================================== int exists_in_attr(string attr_id, string val) { string q; q = "SELECT TOP 1 1 FROM ebs_vr_item_attr WHERE attr_id='" + attr_id + "' AND CONVERT(NVARCHAR(500),attr_val) LIKE '%" + val + "%'"; db_run(q); if(db_row_count() > 0) { return 1; } return 0; }; int exists_in_col(string col_name, string val) { string q; q = "SELECT TOP 1 1 FROM ebs_vr_item WHERE " + col_name + " LIKE '%" + val + "%'"; db_run(q); if(db_row_count() > 0) { return 1; } return 0; }; int func_can_run() { return 1; }; // ========================================== // 筛选字段定义(共23个,对应需求列表,"工单号"原列表 // 重复出现一次,这里去重只保留1个) // ========================================== int func_def_lmt() { string yxqy_uid, yxqy_sql, eba_uid, eba_sql; int yxqy_cnt, eba_cnt; def_lmt_date("lmt_beg_date","开始日期"); def_lmt_date("lmt_end_date","结束日期"); // 客户下拉:跟营销区域一样按登录账号"限定营销区域"权限过滤。账号分配了 // 营销区域(mup_user_bo, bo_id='eba_service')时,客户下拉只列这些区域下的 // 客户(eba.service_id在允许范围内);没分配=不受限,显示全部客户。 // 跟func_query第2节数据级权限管控同源,保证下拉可选项与可查数据一致。 // V45:下拉只显示客户编码eba_id,不再转换显示为客户名称eba_name--两个分支 // 的字典查询第二列(显示列)都从eba_name改成eba_id;不受限分支也不再用内置 // eba字典(那个会显示名称),改成同样只查eba_id的自定义字典。存的值仍是 // eba_id(第一列),筛选匹配eba.eba_id不受影响。 eba_uid = get_user_id(); db_run("select val from mup_user_bo where bo_id='eba_service' and user_id='" + eba_uid + "'"); eba_cnt = db_row_count(); if(eba_cnt > 0) { eba_sql = "select eba_id, eba_id as eba_disp from eba "; eba_sql = eba_sql + "where service_id in ("; eba_sql = eba_sql + "select val from mup_user_bo "; eba_sql = eba_sql + "where bo_id='eba_service' "; eba_sql = eba_sql + "and user_id='" + eba_uid + "') "; eba_sql = eba_sql + "order by eba_id"; dt_define('lmt_eba_dict', eba_sql); def_lmt_dict("lmt_eba_id","客户","lmt_eba_dict"); } else { eba_sql = "select eba_id, eba_id as eba_disp from eba order by eba_id"; dt_define('lmt_eba_dict', eba_sql); def_lmt_dict("lmt_eba_id","客户","lmt_eba_dict"); } def_lmt_str("lmt_gdh","工单号"); def_lmt_str("lmt_nbdh","内部订号"); def_lmt_str("lmt_khcgdh","客户采购单号"); def_lmt_str("lmt_khwlbh","客户物料编号"); def_lmt_str("lmt_khxt","客户型体"); def_lmt_str("lmt_khzl","客户指令"); dt_define('lmt_res_dict', "select res_id, res_name from res where left(res_cat_id,2)='01'"); def_lmt_dict("lmt_nbwlbh","内部料号","lmt_res_dict"); def_lmt_str("lmt_color","通用色号"); def_lmt_str("lmt_nbsh","内部色号"); def_lmt_str("lmt_ppms","品名描述"); dt_define('lmt_dw_dict', "select code, name from app_dict_def where dict_id='res_unit_type' and code in ('A','B','D','E','G')"); def_lmt_dict("lmt_dw","订单单位","lmt_dw_dict"); def_lmt_str("lmt_sfdqry","是否打确认样"); def_lmt_str("lmt_sffhx","是否防虹吸"); def_lmt_str("lmt_dgjp","订购胶片"); def_lmt_str("lmt_zdys","织带印刷"); def_lmt_str("lmt_khddbz","客户订单备注"); def_lmt_str("lmt_mxbz","明细备注"); // 营销区域下拉:按登录账号的"限定营销区域"权限过滤。跟func_query第2节 // 数据级权限管控同源(mup_user_bo, bo_id='eba_service'),保证下拉可选项 // 与实际可查数据范围一致。分配了区域=受限,下拉只列已分配的;没分配= // 不受限,沿用内置eba_service字典显示全部。 yxqy_uid = get_user_id(); db_run("select val from mup_user_bo where bo_id='eba_service' and user_id='" + yxqy_uid + "'"); yxqy_cnt = db_row_count(); if(yxqy_cnt > 0) { yxqy_sql = "select service_id, service_name from eba_service "; yxqy_sql = yxqy_sql + "where service_id in ("; yxqy_sql = yxqy_sql + "select val from mup_user_bo "; yxqy_sql = yxqy_sql + "where bo_id='eba_service' "; yxqy_sql = yxqy_sql + "and user_id='" + yxqy_uid + "') "; yxqy_sql = yxqy_sql + "order by service_id"; dt_define('lmt_yxqy_dict', yxqy_sql); def_lmt_dict("lmt_yxqy","营销区域","lmt_yxqy_dict"); } else { def_lmt_dict("lmt_yxqy","营销区域","eba_service"); } dt_define('lmt_maker_dict','select user_id, user_name from mup_user'); def_lmt_dict("lmt_zdr","制单人","lmt_maker_dict"); def_lmt_dict("lmt_shr","审核人","lmt_maker_dict"); def_lmt_dict("lmt_chzt","出货状态","chzt"); dt_define('lmt_uname_dict', "select distinct uname, uname from res where uname is not null and len(uname) > 0 and left(res_cat_id,2)='01'"); def_lmt_dict("lmt_xm","成品项目","lmt_uname_dict"); return 1; }; int func_init_lmt_val() { // 开始/结束日期默认当天,避免默认查全月导致范围过大、 // 又碰到之前反馈的几万条卡顿问题,需要更早数据手工改 def_lmt_set_val('lmt_beg_date', fmt_date(get_serv_date(), 'yyyymmdd')); lmt_set_checked("lmt_beg_date"); def_lmt_set_val('lmt_end_date', fmt_date(get_serv_date(), 'yyyymmdd')); lmt_set_checked("lmt_end_date"); return 1; }; // ========================================== // 【导出版.报表A】网格只作单列,用于显示"导出结果提示语"。 // 列数=1,须与 func_query 末尾 db_run("SELECT N'...' AS 结果") 一致, // 否则框架报 "dataset is not empty ... set field num fail"(见《DLL编写规范》1.2)。 // ========================================== int func_def_field() { db_set_field_count(1); db_set_field_info(0,'结果',"结果",600); return 1; }; int func_def_imp() { return 1; }; // ========================================== // V31:取消分页钩子--fun_GDYZ_CreatePageContrl这个分页控件依赖 // 报表设计器里单独配置的报表脚本定义(B2采购收货明细走的是那种 // rep_change()/网格设计器架构),自定义报表(func_def_lmt/func_query // 架构)没有这个入口,加了也不生效,只是摆设。之前留着这行没删是 // 因为一直没有实测验证(见V1注释),这次16575条大批量测试时,内置 // 网格在没有分页兜底的情况下硬扛渲染,ERP引擎自身运行时(CC32280MT. // DLL)报Access violation崩溃,间接证实了分页确实没生效--干脆去掉, // 不留一段不起作用的死代码。这份报表本身没有分页能力,数据量大时 // 网格渲染/引擎崩溃的风险还在,只能靠筛选条件(日期范围等)把单次 // 提取量控制在合理规模,没有更好的兜底办法 // ========================================== int func_init() { return 1; }; int func_query() { // ========================================== // 1. 变量声明与初始化 // ========================================== string sql, sql_where; string m_ksrq, m_jsrq; string m_eba_id, m_gdh, m_nbdh, m_khcgdh, m_khwlbh, m_khxt, m_khzl; string m_nbwlbh, m_color, m_nbsh, m_ppms, m_dw, m_sfdqry, m_sffhx; string m_dgjp, m_zdys, m_khddbz, m_mxbz, m_yxqy, m_zdr, m_shr, m_chzt, m_xm; string m_eba_dispname, m_dw_dispname, m_chzt_dispname, m_nbwlbh_dispname; string m_lmt_summary; string uid, m_khbhss; string sql2, t_inp, t_ship, t_wait, t_ass; string m_kssk, m_jssk, m_tqfm; num m_dqsks, m_jssks, m_tqsj, m_tqfz, m_tqms; int count, i, counts, v_has_power; string h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11,h12,h13,h14,h15,h16,h17,h18,h19,h20,h21,h22; string header22, m_sql_full, m_je_setup, exp_home, exp_src, exp_out, cfg_path, qr_person, qr_date; string exp_home2, exp_src2, exp_out2; string param_blob, param_path, dll_msg, exp_result_txt, need_print_msg; int dll_ret, need_print; string EXPECTED_DLL_VER, dll_local_path, cur_dll_ver, dll_update_msg, dll_new_path; string EXPECTED_CFG_VER, cfg_local_path, cur_cfg_ver, cfg_new_path; int need_dll_update, dll_exists, dll_ver_ret, relogin_choice, cfg_need, cfg_ver_ret; int exp_choice, m_grid_t0, m_grid_t1, m_grid_sec, m_grid_rows; m_kssk = fmt_time(get_serv_time(), 'hh:mi:ss'); m_dqsks = time_to_second(get_serv_time()); // ========================================== // 阶段0: 在线维护自检(方便统一更新) // A) 导出DLL(cosd_direct_export.dll):本地缺失 或 版本≠文档最新 → // 弹提示,同意后从文档下载覆盖并重新登录客户端; // B) 连接配置(cosd_dbconn.ini):版本≠最新 → 后台静默从文档下载覆盖 // (不弹窗、不重登),方便改库地址/账号后无感更新。 // 文档管理里"资料名称"必须正好叫 cosd_direct_export.dll / cosd_dbconn.ini。 // ========================================== EXPECTED_DLL_VER = 'direct-2026081320'; dll_local_path = get_home_path() + "\dll\cosd_direct_export.dll"; need_dll_update = 0; dll_exists = file_exist_check(dll_local_path); if(dll_exists == 0) { need_dll_update = 1; dll_update_msg = '检测到当前客户端没有导出DLL(cosd_direct_export.dll),最新版本【' + EXPECTED_DLL_VER + '】,必须先下载安装才能导出,是否现在自动下载并重新登录系统?'; } else { dll_ver_ret = call_user_dll("cosd_direct_export.dll", "get_dll_version", ""); cur_dll_ver = get_user_dll_result(); if(dll_ver_ret != 1 || cur_dll_ver != EXPECTED_DLL_VER) { need_dll_update = 1; if(dll_ver_ret != 1) { cur_dll_ver = '未知(旧版DLL没有版本接口)'; } dll_update_msg = '检测到导出DLL版本【' + cur_dll_ver + '】不是最新【' + EXPECTED_DLL_VER + '】,必须更新后才能导出,是否现在自动更新并重新登录系统?'; } } if(need_dll_update == 1) { relogin_choice = confirm(dll_update_msg); if(relogin_choice == 1) { dll_new_path = get_edoc_file("AAAA","cosd_direct_export.dll"); if(dll_exists == 1) { if(file_unlink(dll_local_path) == 1) { file_rename(dll_new_path, dll_local_path); } } else { file_rename(dll_new_path, dll_local_path); } call_dll_func('gui_app','Gui_App_Restart'); return 1; } else { msg('已取消,DLL未更新,本次无法导出'); return 1; } } EXPECTED_CFG_VER = '2026081301'; cfg_local_path = get_home_path() + "\dll\cosd_dbconn.ini"; cfg_need = 0; if(file_exist_check(cfg_local_path) == 0) { cfg_need = 1; } else { cfg_ver_ret = call_user_dll("cosd_direct_export.dll", "get_config_version", cfg_local_path); cur_cfg_ver = get_user_dll_result(); if(cfg_ver_ret != 1 || cur_cfg_ver != EXPECTED_CFG_VER) { cfg_need = 1; } } if(cfg_need == 1) { cfg_new_path = get_edoc_file("AAAA","cosd_dbconn.ini"); if(file_exist_check(cfg_local_path) == 1) { file_unlink(cfg_local_path); } file_rename(cfg_new_path, cfg_local_path); } m_ksrq = fmt_date(lmt_val('lmt_beg_date'), 'yyyymmdd'); m_jsrq = fmt_date(lmt_val('lmt_end_date'), 'yyyymmdd'); m_eba_id = lmt_val('lmt_eba_id'); m_gdh = lmt_val('lmt_gdh'); m_nbdh = lmt_val('lmt_nbdh'); m_khcgdh = lmt_val('lmt_khcgdh'); m_khwlbh = lmt_val('lmt_khwlbh'); m_khxt = lmt_val('lmt_khxt'); m_khzl = lmt_val('lmt_khzl'); m_nbwlbh = lmt_val('lmt_nbwlbh'); m_color = lmt_val('lmt_color'); m_nbsh = lmt_val('lmt_nbsh'); m_ppms = lmt_val('lmt_ppms'); m_dw = lmt_val('lmt_dw'); m_sfdqry = lmt_val('lmt_sfdqry'); m_sffhx = lmt_val('lmt_sffhx'); m_dgjp = lmt_val('lmt_dgjp'); m_zdys = lmt_val('lmt_zdys'); m_khddbz = lmt_val('lmt_khddbz'); m_mxbz = lmt_val('lmt_mxbz'); m_yxqy = lmt_val('lmt_yxqy'); m_zdr = lmt_val('lmt_zdr'); m_shr = lmt_val('lmt_shr'); m_chzt = lmt_val('lmt_chzt'); m_xm = lmt_val('lmt_xm'); // ========================================== // 1.5 字典型筛选值转显示名称(导出模版表头要显示文字,不是编码)。 // 这几个db_run()必须放在主查询db_run(sql+sql_where+ORDER BY)之前 // 执行--APPEND阶段是靠db_go_head()/db_next()在主查询的结果集上 // 遍历的,如果在主查询之后、APPEND之前插入别的db_run(),会把当前 // 结果集换成这些小查询的结果,APPEND就读错数据了。每次查完立刻用 // db_row_count()/db_go_row()/db_res()取值,不留到后面用 // ========================================== // V46:客户不再转换显示为eba_name,导出表头/筛选条件说明直接用编码m_eba_id // (去掉了原来查eba_name的那段db_run)。这样导出Excel的"客户"表头(R4)显示 // 的就是客户编码,跟检索条件下拉(V45已改成只显示eba_id)保持一致 m_eba_dispname = m_eba_id; m_dw_dispname = m_dw; if(m_dw != "") { db_run("SELECT name FROM app_dict_def WHERE dict_id='res_unit_type' AND code='" + m_dw + "'"); if(db_row_count() > 0) { db_go_row(0); m_dw_dispname = db_res(0); } } m_chzt_dispname = m_chzt; if(m_chzt != "") { db_run("SELECT name FROM app_dict_def WHERE dict_id='chzt' AND code='" + m_chzt + "'"); if(db_row_count() > 0) { db_go_row(0); m_chzt_dispname = db_res(0); } } m_nbwlbh_dispname = m_nbwlbh; if(m_nbwlbh != "") { db_run("SELECT res_name FROM res WHERE res_id='" + m_nbwlbh + "'"); if(db_row_count() > 0) { db_go_row(0); m_nbwlbh_dispname = db_res(0); } } // ========================================== // 1.6 文本型筛选框存在性校验:勾选且填了值的,先单独按这个 // 内容去数据库查一下,查不到就直接弹提示、终止提取,不再往 // 下走主查询--避免用户输错内容/没这个数据时,只看到笼统的 // "没有查询到符合条件的..."提示,分不清是筛选条件本身没有 // 这个值,还是有这个值但跟其它筛选/日期范围凑不到一起 // ========================================== if(lmt_checked('lmt_gdh') && m_gdh != "" && exists_in_attr('gdh', m_gdh) == 0) { msg('工单号【' + m_gdh + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_nbdh') && m_nbdh != "" && exists_in_col('vr_item_ext_2', m_nbdh) == 0) { msg('内部订号【' + m_nbdh + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_khcgdh') && m_khcgdh != "" && exists_in_col('vr_item_ext_3', m_khcgdh) == 0) { msg('客户采购单号【' + m_khcgdh + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_khwlbh') && m_khwlbh != "" && exists_in_attr('khwldm', m_khwlbh) == 0) { msg('客户物料编号【' + m_khwlbh + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_khxt') && m_khxt != "" && exists_in_col('vr_item_ext_4', m_khxt) == 0) { msg('客户型体【' + m_khxt + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_khzl') && m_khzl != "" && exists_in_attr('khzl', m_khzl) == 0) { msg('客户指令【' + m_khzl + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_color') && m_color != "" && exists_in_attr('color', m_color) == 0) { msg('通用色号【' + m_color + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_nbsh') && m_nbsh != "" && exists_in_attr('nbsh', m_nbsh) == 0) { msg('内部色号【' + m_nbsh + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_ppms') && m_ppms != "" && exists_in_col('vr_item_ext_6', m_ppms) == 0) { msg('品名描述【' + m_ppms + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_sfdqry') && m_sfdqry != "" && exists_in_attr('sfdqry', m_sfdqry) == 0) { msg('是否打确认样【' + m_sfdqry + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_sffhx') && m_sffhx != "" && exists_in_attr('sffhx', m_sffhx) == 0) { msg('是否防虹吸【' + m_sffhx + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_dgjp') && m_dgjp != "" && exists_in_attr('dgjp', m_dgjp) == 0) { msg('订购胶片【' + m_dgjp + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_zdys') && m_zdys != "" && exists_in_attr('zdys', m_zdys) == 0) { msg('织带印刷【' + m_zdys + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_khddbz') && m_khddbz != "" && exists_in_attr('khddbz', m_khddbz) == 0) { msg('客户订单备注【' + m_khddbz + '】没有查询到对应的数据,请检查输入内容'); return 1; } if(lmt_checked('lmt_mxbz') && m_mxbz != "" && exists_in_col('note_info', m_mxbz) == 0) { msg('明细备注【' + m_mxbz + '】没有查询到对应的数据,请检查输入内容'); return 1; } // ========================================== // 1.7 汇总一份"当前实际生效的筛选条件"文字说明,只列勾选且有值的, // 用在下面主查询查不到数据时的提示里--多个筛选条件组合在一起查 // 不到数据时,不用挨个回去看界面上到底勾了哪些框、填了什么值,提示 // 里直接列出来,方便定位到底是哪个条件把范围收窄没数据了 // ========================================== m_lmt_summary = ""; if(lmt_checked('lmt_eba_id') && m_eba_id != "") { m_lmt_summary += "客户:" + m_eba_dispname + "; "; } if(lmt_checked('lmt_gdh') && m_gdh != "") { m_lmt_summary += "工单号:" + m_gdh + "; "; } if(lmt_checked('lmt_nbdh') && m_nbdh != "") { m_lmt_summary += "内部订号:" + m_nbdh + "; "; } if(lmt_checked('lmt_khcgdh') && m_khcgdh != "") { m_lmt_summary += "客户采购单号:" + m_khcgdh + "; "; } if(lmt_checked('lmt_khwlbh') && m_khwlbh != "") { m_lmt_summary += "客户物料编号:" + m_khwlbh + "; "; } if(lmt_checked('lmt_khxt') && m_khxt != "") { m_lmt_summary += "客户型体:" + m_khxt + "; "; } if(lmt_checked('lmt_khzl') && m_khzl != "") { m_lmt_summary += "客户指令:" + m_khzl + "; "; } if(lmt_checked('lmt_nbwlbh') && m_nbwlbh != "") { m_lmt_summary += "内部料号:" + m_nbwlbh_dispname + "; "; } if(lmt_checked('lmt_color') && m_color != "") { m_lmt_summary += "通用色号:" + m_color + "; "; } if(lmt_checked('lmt_nbsh') && m_nbsh != "") { m_lmt_summary += "内部色号:" + m_nbsh + "; "; } if(lmt_checked('lmt_ppms') && m_ppms != "") { m_lmt_summary += "品名描述:" + m_ppms + "; "; } if(lmt_checked('lmt_dw') && m_dw != "") { m_lmt_summary += "订单单位:" + m_dw_dispname + "; "; } if(lmt_checked('lmt_sfdqry') && m_sfdqry != "") { m_lmt_summary += "是否打确认样:" + m_sfdqry + "; "; } if(lmt_checked('lmt_sffhx') && m_sffhx != "") { m_lmt_summary += "是否防虹吸:" + m_sffhx + "; "; } if(lmt_checked('lmt_dgjp') && m_dgjp != "") { m_lmt_summary += "订购胶片:" + m_dgjp + "; "; } if(lmt_checked('lmt_zdys') && m_zdys != "") { m_lmt_summary += "织带印刷:" + m_zdys + "; "; } if(lmt_checked('lmt_khddbz') && m_khddbz != "") { m_lmt_summary += "客户订单备注:" + m_khddbz + "; "; } if(lmt_checked('lmt_mxbz') && m_mxbz != "") { m_lmt_summary += "明细备注:" + m_mxbz + "; "; } if(lmt_checked('lmt_yxqy') && m_yxqy != "") { m_lmt_summary += "营销区域:" + m_yxqy + "; "; } if(lmt_checked('lmt_zdr') && m_zdr != "") { m_lmt_summary += "制单人:" + m_zdr + "; "; } if(lmt_checked('lmt_shr') && m_shr != "") { m_lmt_summary += "审核人:" + m_shr + "; "; } if(lmt_checked('lmt_chzt') && m_chzt != "") { m_lmt_summary += "出货状态:" + m_chzt_dispname + "; "; } if(lmt_checked('lmt_xm') && m_xm != "") { m_lmt_summary += "成品项目:" + m_xm + "; "; } // ========================================== // 2. 营销区域权限管控(照抄底稿脚本的权限逻辑): // 当前用户在mup_user_bo里被分配了哪些eba_service // (营销区域),客户范围就限定在这些区域下的客户; // 未分配任何区域视为不受限(v_has_power=0) // ========================================== uid = get_user_id(); m_khbhss = ""; db_run("select val from mup_user_bo where bo_id='eba_service' and user_id='" + uid + "'"); count = db_row_count(); if(count > 0) { v_has_power = 1; for(i = 0; i < count; i++) { db_go_row(i); if(m_khbhss == "") { m_khbhss = db_res(0); } else { m_khbhss = m_khbhss + "," + db_res(0); } } } else { v_has_power = 0; } // ========================================== // 3. 构建查询SQL(在底稿基础上补充客户名称/营销区域, // 补充PIVOT属性表的字段作为筛选来源)。SELECT字段列表 //拆成一行一个字段,每行独立sql+= "...";闭合,方便肉眼 // 核对/避免粘贴时漏内容不容易被发现 // ========================================== sql = "SELECT ROW_NUMBER() OVER (ORDER BY d.voucher_no, a.item_id) AS 序号, "; sql+= "d.voucher_no AS 订单单据编号, "; sql+= "STUFF(STUFF(CAST(d.voucher_date AS VARCHAR(10)), 7, 0, '-'), 5, 0, '-') AS 单据日期, "; sql+= "g.user_name AS 制单人, "; sql+= "i.name AS 单据状态, "; sql+= "h.user_name AS 审核人, "; sql+= "e.eba_id AS 客户编号, "; sql+= "j.name AS 业务员, "; sql+= "a.vr_item_ext_2 AS 内部订号, "; sql+= "CASE T.djlx WHEN 'BA01' THEN '量产' WHEN 'BA04' THEN '销样' ELSE T.djlx END AS 订单类型, "; sql+= "k.res_spec AS 生产部门, "; sql+= "k.uname AS 成品项次, "; sql+= "T.gdh AS 工单号, "; sql+= "T.yggdh AS 预告工单号, "; sql+= "a.vr_item_ext_1 AS 下单日期, "; sql+= "STUFF(STUFF(CAST(a.item_date_lmt AS VARCHAR(10)), 7, 0, '-'), 5, 0, '-') AS 交付日期, "; sql+= "a.vr_item_ext_3 AS 客户采购单号, "; sql+= "a.vr_item_ext_4 AS 客户型体, "; sql+= "a.vr_item_ext_5 AS 项次, "; sql+= "T.khzl AS 客户指令, "; sql+= "T.qhdh AS 企划单号, "; sql+= "T.qydh AS 企业单号, "; sql+= "T.khwldm AS 客户物料编号, "; sql+= "a.res_id AS 内部料号, "; sql+= "a.vr_item_ext_6 AS 品名描述, "; sql+= "a.sub_attr_val_1 AS 规格, "; sql+= "T.color AS 通用色号, "; sql+= "T.nbsh AS 内部色号, "; sql+= "T.changdu AS 长度, "; sql+= "T.sfdqry AS 是否打确认样, "; sql+= "T.sffhx AS 是否防虹吸, "; sql+= "T.dgjp AS 订购胶片, "; sql+= "T.zdys AS 织带印刷, "; sql+= "a.inp_num AS 订单数量, "; sql+= "ISNULL(Ship.total_ship_num, 0) AS 已出货数量, "; sql+= "(a.inp_num - ISNULL(Ship.total_ship_num, 0)) AS 待出货数量, "; sql+= "f.name AS 订单单位, "; sql+= "T.khddbz AS 客户订单备注, "; sql+= "a.note_info AS 备注, "; sql+= "a.ass_num AS [生产数量(Y)], "; // 【性能修复 v63】基表先物化成 #base(不含JE三列、不含#jeagg join),避免 // "LEFT JOIN #jeagg ON JEA.gdh_key=T.gdh" 打断 PIVOT 的日期下推(否则整张 // ebs_vr_item_attr 会被全表透视+LOB转换,39519行要116秒)。只加一个关联键 // je_key=CONVERT(NVARCHAR(100),T.gdh),与 #jeagg.gdh_key 同型;JE三列改到最后 // 一步 #base LEFT JOIN #jeagg 时再取(见 m_sql_full 拼装处)。 sql+= "CONVERT(NVARCHAR(100),T.gdh) AS je_key "; sql+= "INTO #base "; sql+= "FROM ebs_vr_item a "; sql+= "INNER JOIN ebs_v d ON d.voucher_id = a.voucher_id "; sql+= "AND d.voucher_type = 'BA' "; sql+= "AND d.voucher_date >= '" + m_ksrq + "' "; sql+= "AND d.voucher_date <= '" + m_jsrq + "' "; sql+= "LEFT JOIN ebs_vr e ON e.voucher_id = a.voucher_id "; sql+= "LEFT JOIN eba ON eba.eba_id = e.eba_id "; // 出货数量聚合 sql+= "LEFT JOIN ( "; sql+= "SELECT b.ref_voucher_id, b.ref_item_id, SUM(b.inp_num) AS total_ship_num "; sql+= "FROM ebs_vr_item b "; sql+= "INNER JOIN ebs_v c ON c.voucher_id = b.voucher_id "; sql+= "WHERE c.voucher_type = 'BB' "; sql+= "AND b.is_main = 'N' "; sql+= "AND b.inp_num > 0 "; sql+= "GROUP BY b.ref_voucher_id, b.ref_item_id "; sql+= ") AS Ship ON a.voucher_id = Ship.ref_voucher_id "; sql+= "AND a.item_id = Ship.ref_item_id "; // PIVOT属性表 sql+= "LEFT JOIN ( "; sql+= "SELECT attr_id, CONVERT(NVARCHAR(500), attr_val) AS attr_val, item_id, voucher_id "; sql+= "FROM ebs_vr_item_attr "; sql+= ") AS S PIVOT ( "; sql+= "MAX(s.attr_val) FOR S.attr_id IN "; sql+= "([khwldm],[color],[nbsh],[sfdqry],[dgjp],[zdys],[sffhx],[pinpai],"; sql+= "[djlx],[changdu],[yggdh],[ddbz],[khddbz],[gdh],[jijie],[qhdh],[qydh],[khzl]) "; sql+= ") AS T ON a.voucher_id = T.voucher_id "; sql+= "AND a.item_id = T.item_id "; // 【性能修复 v63】#jeagg 不在此处 join(会打断 PIVOT 下推),改到最后一步对 #base 做。 // 基础字典及人员表关联 sql+= "LEFT JOIN app_dict_def f ON f.dict_id='res_unit_type' "; sql+= "AND f.code=a.inp_unit_type_id "; sql+= "LEFT JOIN mup_user g ON g.user_id=d.create_user_id "; sql+= "LEFT JOIN mup_user h ON h.user_id=d.check_user_id "; sql+= "LEFT JOIN app_dict_def i ON i.dict_id='voucher_state' "; sql+= "AND i.code=d.state "; sql+= "LEFT JOIN emp j ON j.emp_id=d.emp_id "; sql+= "LEFT JOIN (SELECT res_id,res_spec,uname FROM res) AS k "; sql+= "ON k.res_id=a.res_id "; // ========================================== // 4. 过滤条件:23个筛选字段,只对勾选且有值的条件 // 拼WHERE,备注类自由文本用LIKE模糊匹配,其余用=精确匹配 // ========================================== sql_where = "WHERE a.inp_num > 0 "; if(v_has_power == 1) { sql_where += "AND eba.service_id IN (SELECT val FROM mup_user_bo WHERE bo_id='eba_service' AND user_id='" + uid + "') "; } if(lmt_checked('lmt_eba_id') && m_eba_id != "") { sql_where += "AND e.eba_id = '" + m_eba_id + "' "; } if(lmt_checked('lmt_gdh') && m_gdh != "") { sql_where += "AND T.gdh LIKE '%" + m_gdh + "%' "; } if(lmt_checked('lmt_nbdh') && m_nbdh != "") { sql_where += "AND a.vr_item_ext_2 LIKE '%" + m_nbdh + "%' "; } if(lmt_checked('lmt_khcgdh') && m_khcgdh != "") { sql_where += "AND a.vr_item_ext_3 LIKE '%" + m_khcgdh + "%' "; } if(lmt_checked('lmt_khwlbh') && m_khwlbh != "") { sql_where += "AND T.khwldm LIKE '%" + m_khwlbh + "%' "; } if(lmt_checked('lmt_khxt') && m_khxt != "") { sql_where += "AND a.vr_item_ext_4 LIKE '%" + m_khxt + "%' "; } if(lmt_checked('lmt_khzl') && m_khzl != "") { sql_where += "AND T.khzl LIKE '%" + m_khzl + "%' "; } if(lmt_checked('lmt_nbwlbh') && m_nbwlbh != "") { sql_where += "AND a.res_id = '" + m_nbwlbh + "' "; } if(lmt_checked('lmt_color') && m_color != "") { sql_where += "AND T.color LIKE '%" + m_color + "%' "; } if(lmt_checked('lmt_nbsh') && m_nbsh != "") { sql_where += "AND T.nbsh LIKE '%" + m_nbsh + "%' "; } if(lmt_checked('lmt_ppms') && m_ppms != "") { sql_where += "AND a.vr_item_ext_6 LIKE '%" + m_ppms + "%' "; } if(lmt_checked('lmt_dw') && m_dw != "") { sql_where += "AND a.inp_unit_type_id = '" + m_dw + "' "; } if(lmt_checked('lmt_sfdqry') && m_sfdqry != "") { sql_where += "AND T.sfdqry LIKE '%" + m_sfdqry + "%' "; } if(lmt_checked('lmt_sffhx') && m_sffhx != "") { sql_where += "AND T.sffhx LIKE '%" + m_sffhx + "%' "; } if(lmt_checked('lmt_dgjp') && m_dgjp != "") { sql_where += "AND T.dgjp LIKE '%" + m_dgjp + "%' "; } if(lmt_checked('lmt_zdys') && m_zdys != "") { sql_where += "AND T.zdys LIKE '%" + m_zdys + "%' "; } if(lmt_checked('lmt_khddbz') && m_khddbz != "") { sql_where += "AND T.khddbz LIKE '%" + m_khddbz + "%' "; } if(lmt_checked('lmt_mxbz') && m_mxbz != "") { sql_where += "AND a.note_info LIKE '%" + m_mxbz + "%' "; } if(lmt_checked('lmt_yxqy') && m_yxqy != "") { sql_where += "AND eba.service_id = '" + m_yxqy + "' "; } if(lmt_checked('lmt_zdr') && m_zdr != "") { sql_where += "AND d.create_user_id = '" + m_zdr + "' "; } if(lmt_checked('lmt_shr') && m_shr != "") { sql_where += "AND d.check_user_id = '" + m_shr + "' "; } if(lmt_checked('lmt_chzt') && m_chzt != "") { if(m_chzt == "A") { sql_where += "AND a.inp_num = ISNULL(Ship.total_ship_num, 0) "; } if(m_chzt == "B") { sql_where += "AND a.inp_num > ISNULL(Ship.total_ship_num, 0) "; sql_where += "AND ISNULL(Ship.total_ship_num, 0) > 0 "; } if(m_chzt == "C") { sql_where += "AND ISNULL(Ship.total_ship_num, 0) = 0 "; } } if(lmt_checked('lmt_xm') && m_xm != "") { sql_where += "AND k.uname = '" + m_xm + "' "; } // ========================================== // 5.【生产直连版 V60】不再脚本逐行db_res取数 + 临时文件 + 分批 + INIT/APPEND/ // FINAL,改由 cosd_direct_export.dll 直连数据库:原生逐行取数(ODBC宽字符, // 中文越南文不丢)、合计在取数时累加、直接写带模板格式的 xlsx。 // 39519条实测约5秒(旧方案5分钟)。主查询不再在脚本里db_run(避免几万行 // 网格渲染拖慢/风险),网格只放占位行。 // ========================================== // ========================================== // 【性能】JE三列两步物化,GROUP BY取值,耗时不随结果行数暴涨: // 1) #jebase:只装"本次日期范围BA订单用到的工单号"对应的JE明细(去重,缩小范围)。 // 2) #jeagg:每工单号取一条上机记录(按上机日期最新),多条时值末尾标" 等"; // 用ROW_NUMBER+COUNT窗口(集合运算,无逐组FOR XML拼接),几万行也快,建索引。 // 背景:FOR XML逐组拼接写法5个月39519条要120秒(瓶颈100%在拼接,库级索引无解); // 改GROUP BY取一条后大幅降。代价:极少数一单多机台的只显示最新一条+" 等"标记。 // 需DLL支持"SET NOCOUNT ON+多语句批处理"(跳过无结果集前置语句,见DLL≥313)。 // gdh_key用NVARCHAR(100):工单号短,避免超900字节索引限制。 // ========================================== // 【性能修复 v63b.关键】SET ARITHABORT ON:老 {SQL Server} ODBC 驱动默认 // ARITHABORT OFF,与 SSMS(默认 ON)不同 → 同一条 SQL 编译出两套计划,OFF // 拿到坏计划(PIVOT 全表扫),39519 行 117 秒;ON 与 SSMS 一致,约 5-8 秒。 // 这是"SSMS 快、程序慢"的经典根因,必须在批处理最前面显式打开。 m_je_setup = "SET ARITHABORT ON; SET NOCOUNT ON; "; m_je_setup = m_je_setup + "IF OBJECT_ID('tempdb..#jebase') IS NOT NULL DROP TABLE #jebase; "; m_je_setup = m_je_setup + "IF OBJECT_ID('tempdb..#jeagg') IS NOT NULL DROP TABLE #jeagg; "; m_je_setup = m_je_setup + "SELECT DISTINCT CONVERT(NVARCHAR(100),a2.attr_val) AS gdh_key, "; m_je_setup = m_je_setup + "i2.ref_voucher_no, v2.voucher_date, i2.batch_no, i2.vr_item_ext_1 "; m_je_setup = m_je_setup + "INTO #jebase "; m_je_setup = m_je_setup + "FROM ebs_vr_item_attr a2 "; m_je_setup = m_je_setup + "INNER JOIN ebs_vr_item i2 ON i2.voucher_id=a2.voucher_id "; m_je_setup = m_je_setup + "AND i2.item_id=a2.item_id "; m_je_setup = m_je_setup + "INNER JOIN ebs_v v2 ON v2.voucher_id=a2.voucher_id "; m_je_setup = m_je_setup + "AND v2.voucher_type='JE' "; m_je_setup = m_je_setup + "WHERE a2.attr_id='gdh' "; // 限定到本次日期范围BA订单用到的工单号(与主查询同一日期区间) m_je_setup = m_je_setup + "AND CONVERT(NVARCHAR(100),a2.attr_val) IN ( "; m_je_setup = m_je_setup + "SELECT CONVERT(NVARCHAR(100),ba.attr_val) "; m_je_setup = m_je_setup + "FROM ebs_vr_item_attr ba "; m_je_setup = m_je_setup + "INNER JOIN ebs_v bv ON bv.voucher_id=ba.voucher_id "; m_je_setup = m_je_setup + "AND bv.voucher_type='BA' "; m_je_setup = m_je_setup + "AND bv.voucher_date>='" + m_ksrq + "' "; m_je_setup = m_je_setup + "AND bv.voucher_date<='" + m_jsrq + "' "; m_je_setup = m_je_setup + "WHERE ba.attr_id='gdh'); "; m_je_setup = m_je_setup + "CREATE INDEX ix_jebase ON #jebase(gdh_key); "; // #jeagg:每工单号取一条上机记录(按上机日期voucher_date最新那条,3列配套同一条); // 若该工单号有多条JE记录,值末尾标" 等",便于看出还有更多。用ROW_NUMBER+COUNT窗口 // (集合运算、无逐组FOR XML拼接),几万行也快。cnt=该工单号的JE记录条数(#jebase已去重)。 m_je_setup = m_je_setup + "WITH ranked AS ( "; m_je_setup = m_je_setup + "SELECT gdh_key, ref_voucher_no, voucher_date, batch_no, vr_item_ext_1, "; m_je_setup = m_je_setup + "ROW_NUMBER() OVER (PARTITION BY gdh_key ORDER BY voucher_date DESC) AS rn, "; m_je_setup = m_je_setup + "COUNT(*) OVER (PARTITION BY gdh_key) AS cnt "; m_je_setup = m_je_setup + "FROM #jebase ) "; m_je_setup = m_je_setup + "SELECT gdh_key, "; m_je_setup= m_je_setup + "ISNULL(CONVERT(NVARCHAR(200),ref_voucher_no),'') "; m_je_setup = m_je_setup + "+ CASE WHEN cnt>1 THEN N' 等' ELSE N'' END AS 指令单号, "; m_je_setup = m_je_setup + "SUBSTRING(CAST(voucher_date AS VARCHAR(8)),1,4)+N'年'+ "; m_je_setup = m_je_setup + "SUBSTRING(CAST(voucher_date AS VARCHAR(8)),5,2)+N'月'+ "; m_je_setup = m_je_setup + "SUBSTRING(CAST(voucher_date AS VARCHAR(8)),7,2)+N'日' "; m_je_setup = m_je_setup + "+ CASE WHEN cnt>1 THEN N' 等' ELSE N'' END AS 上机日期, "; m_je_setup = m_je_setup + "ISNULL(CONVERT(NVARCHAR(200),batch_no),'') "; m_je_setup = m_je_setup + "+ ISNULL(CONVERT(NVARCHAR(200),vr_item_ext_1),'') "; m_je_setup = m_je_setup + "+ CASE WHEN cnt>1 THEN N' 等' ELSE N'' END AS 机器编号名称 "; m_je_setup = m_je_setup + "INTO #jeagg "; m_je_setup = m_je_setup + "FROM ranked WHERE rn=1; "; m_je_setup = m_je_setup + "CREATE INDEX ix_jeagg ON #jeagg(gdh_key); "; // 【性能修复 v63】三段式: // 1) m_je_setup 建 #jebase/#jeagg(实测<1秒); // 2) sql+sql_where 把基表(不含JE)SELECT ... INTO #base -- 此时 PIVOT 日期 // 下推正常,约7秒;结果只 3.9 万行; // 3) 最后 #base LEFT JOIN #jeagg 取 JE 三列(3.9万×135行哈希连接,瞬间)。 // 总耗时≈基表耗时(≈7-8秒),不再是 116 秒。 // 最终结果集 43 列,列序与旧版完全一致(0..39 明细 + 40指令单号/41上机日期/42机器编号名称), // DLL 的 ncols==43 与模版 .. 均不受影响。 m_sql_full = m_je_setup + sql + sql_where + "; "; // 【性能修复 v315】分界标记:DLL 在此把"建临时表"与"最终SELECT"拆成两次执行, // 让最终SELECT单条走 firehose 整包取数(否则多语句批处理→服务器游标→逐行往返, // 内网81秒/VPN115秒)。标记是SQL注释,旧DLL整段执行也无害(向后兼容)。需DLL≥315。 m_sql_full = m_sql_full + "/*__EXECSPLIT__*/"; m_sql_full = m_sql_full + "SELECT 序号,订单单据编号,单据日期,制单人,单据状态,审核人,"; m_sql_full = m_sql_full + "客户编号,业务员,内部订号,订单类型,生产部门,成品项次,工单号,"; m_sql_full = m_sql_full + "预告工单号,下单日期,交付日期,客户采购单号,客户型体,项次,客户指令,"; m_sql_full = m_sql_full + "企划单号,企业单号,客户物料编号,内部料号,品名描述,规格,通用色号,"; m_sql_full = m_sql_full + "内部色号,长度,是否打确认样,是否防虹吸,订购胶片,织带印刷,订单数量,"; m_sql_full = m_sql_full + "已出货数量,待出货数量,订单单位,客户订单备注,备注,[生产数量(Y)], "; m_sql_full = m_sql_full + "JEA.指令单号 AS 指令单号, JEA.上机日期 AS 上机日期, "; m_sql_full = m_sql_full + "JEA.机器编号名称 AS 机器编号名称 "; m_sql_full = m_sql_full + "FROM #base LEFT JOIN #jeagg JEA ON JEA.gdh_key = #base.je_key "; m_sql_full = m_sql_full + "ORDER BY 序号"; // ========================================== // 【导出版.报表A】打开报表即导出Excel文档(不在网格显示明细)。 // 导出成功/失败后,把结果提示语用db_run写进单列网格显示(见func_query末尾)。 // ========================================== // 22个条件回显(顺序必须与模版表头H(0)..H(21)一致) h1 = fmt_date(m_ksrq,'yyyy-mm-dd'); h2 = fmt_date(m_jsrq,'yyyy-mm-dd'); h3 = ""; if(lmt_checked('lmt_eba_id') && m_eba_id != "") { h3 = m_eba_dispname; } h4 = ""; if(lmt_checked('lmt_nbdh') && m_nbdh != "") { h4 = m_nbdh; } h5 = ""; if(lmt_checked('lmt_nbwlbh') && m_nbwlbh != "") { h5 = m_nbwlbh_dispname; } h6 = ""; if(lmt_checked('lmt_khwlbh') && m_khwlbh != "") { h6 = m_khwlbh; } h7 = ""; if(lmt_checked('lmt_gdh') && m_gdh != "") { h7 = m_gdh; } h8 = ""; h9 = ""; if(lmt_checked('lmt_khcgdh') && m_khcgdh != "") { h9 = m_khcgdh; } h10 = ""; if(lmt_checked('lmt_khzl') && m_khzl != "") { h10 = m_khzl; } h11 = ""; if(lmt_checked('lmt_khxt') && m_khxt != "") { h11 = m_khxt; } h12 = ""; if(lmt_checked('lmt_color') && m_color != "") { h12 = m_color; } h13 = ""; if(lmt_checked('lmt_nbsh') && m_nbsh != "") { h13 = m_nbsh; } h14 = ""; if(lmt_checked('lmt_sfdqry') && m_sfdqry != "") { h14 = m_sfdqry; } h15 = ""; if(lmt_checked('lmt_sffhx') && m_sffhx != "") { h15 = m_sffhx; } h16 = ""; if(lmt_checked('lmt_dgjp') && m_dgjp != "") { h16 = m_dgjp; } h17 = ""; if(lmt_checked('lmt_zdys') && m_zdys != "") { h17 = m_zdys; } h18 = ""; if(lmt_checked('lmt_khddbz') && m_khddbz != "") { h18 = m_khddbz; } h19 = ""; if(lmt_checked('lmt_mxbz') && m_mxbz != "") { h19 = m_mxbz; } h20 = ""; if(lmt_checked('lmt_ppms') && m_ppms != "") { h20 = m_ppms; } h21 = ""; if(lmt_checked('lmt_dw') && m_dw != "") { h21 = m_dw_dispname; } h22 = ""; if(lmt_checked('lmt_chzt') && m_chzt != "") { h22 = m_chzt_dispname; } header22 = h1 + '|~|' + h2 + '|~|' + h3 + '|~|' + h4 + '|~|' + h5 + '|~|' + h6; header22 = header22 + '|~|' + h7 + '|~|' + h8 + '|~|' + h9 + '|~|' + h10 + '|~|' + h11 + '|~|' + h12; header22 = header22 + '|~|' + h13 + '|~|' + h14 + '|~|' + h15 + '|~|' + h16 + '|~|' + h17 + '|~|' + h18; header22 = header22 + '|~|' + h19 + '|~|' + h20 + '|~|' + h21 + '|~|' + h22; // 打印版使用频率低:先问是否需要导出打印版。是=打印版+查询版都导出;否=只导出查询版。 // need_print=1(是) / 0(否)。confirm 与自更新处 relogin_choice 同一约定:是返回1。 // 中越双语确认框(越南文带声调)。若客户端弹窗把声调字显示成"?",说明UI走GBK, // 改用无声调ASCII备选(见《DLL编写规范》10.2)。带声调不影响脚本编译。 // 长文案拆成短串拼接,避免整行过长被ERP编辑器粘贴截断(致"引号没有匹配") need_print_msg = '是否导出【打印版】? 打印版使用较少。'; need_print_msg = need_print_msg + '[是]=打印版+查询版都导出,[否]=只导出查询版。'; // 越南文带声调版:本客户端脚本编辑器保存走UTF-8,声调字(ả ấ ầ ô ì…)经"保存+ // 重登重载"实测不截断、弹窗完整显示(见《DLL编写规范》10.2)。若换到按GBK保存的 // 编辑器出现"引号没有匹配"或声调变"?",改回无声调ASCII备选。 need_print_msg = need_print_msg + ' / Bản in ít dùng, lần này có cần xuất [Bản in] không? '; need_print_msg = need_print_msg + '[Có / 是]=xuất cả bản in và bản truy vấn; '; need_print_msg = need_print_msg + '[Không / 否]=chỉ xuất bản truy vấn.'; need_print = confirm(need_print_msg); // 导出/临时文件统一放 tmp 目录(客户端重登会自动清空,避免文件在硬盘上累加) // 只有需要打印版时才下载主模版、才设打印版输出路径;否则两者置空,DLL据此跳过打印版 exp_home = ''; exp_out = ''; if(need_print == 1) { // 取主模版到 tmp 目录(带标题+查询条件) exp_home = get_home_path() + "\tmp\客户订单明细表导出模版.xlsx"; exp_src = get_edoc_file("AAAA","客户订单明细表导出模版.xlsx"); if(file_exist_check(exp_home) == 1) { if(file_unlink(exp_home) == 1) { file_rename(exp_src, exp_home); } } else { file_rename(exp_src, exp_home); } exp_out = get_home_path() + "\tmp\客户订单出货明细【打印版】_"; exp_out = exp_out + fmt_date(get_serv_date(),'yyyy年mm月dd日') + "_"; exp_out = exp_out + fmt_time(get_serv_time(),'hh时mi分ss秒') + ".xlsx"; } // 取纯净模版到 tmp 目录(无标题无检索、宽列)。文档管理"资料名称"须正好叫此名。查询版始终导出。 exp_home2 = get_home_path() + "\tmp\客户订单明细表导出模版_无表头和检索内容.xlsx"; exp_src2 = get_edoc_file("AAAA","客户订单明细表导出模版_无表头和检索内容.xlsx"); if(file_exist_check(exp_home2) == 1) { if(file_unlink(exp_home2) == 1) { file_rename(exp_src2, exp_home2); } } else { file_rename(exp_src2, exp_home2); } exp_out2 = get_home_path() + "\tmp\客户订单出货明细【查询版】_"; exp_out2 = exp_out2 + fmt_date(get_serv_date(),'yyyy年mm月dd日') + "_"; exp_out2 = exp_out2 + fmt_time(get_serv_time(),'hh时mi分ss秒') + ".xlsx"; cfg_path = get_home_path() + "\dll\cosd_dbconn.ini"; qr_person = get_user_id() + ' ' + get_user_name(); qr_date = fmt_date(get_serv_date(),'yyyy-mm-dd') + ' ' + fmt_time(get_serv_time(),'hh:mi:ss'); // 写参数文件(9行:配置/主模版/主输出/查询人/查询日期/22条件回显/纯净模版/纯净输出/SQL) // 参数用唯一分隔符|~~PARAM~~|拼接(字段里即使含换行也不会错位;DLL按同分隔符切) param_blob = cfg_path + "|~~PARAM~~|"; param_blob = param_blob + exp_home + "|~~PARAM~~|"; param_blob = param_blob + exp_out + "|~~PARAM~~|"; param_blob = param_blob + qr_person + "|~~PARAM~~|"; param_blob = param_blob + qr_date + "|~~PARAM~~|"; param_blob = param_blob + header22 + "|~~PARAM~~|"; param_blob = param_blob + exp_home2 + "|~~PARAM~~|"; param_blob = param_blob + exp_out2 + "|~~PARAM~~|"; param_blob = param_blob + m_sql_full; param_path = get_home_path() + "\tmp\cosd_direct_param.txt"; file_write(param_path, param_blob); m_dqsks = time_to_second(get_serv_time()); dll_ret = call_user_dll("cosd_direct_export.dll", "export_direct", param_path); dll_msg = get_user_dll_result(); m_jssks = time_to_second(get_serv_time()); m_tqsj = m_jssks - m_dqsks; if(file_exist_check(param_path) == 1) { file_unlink(param_path); } if(need_print == 1) { if(file_exist_check(exp_home) == 1) { file_unlink(exp_home); } } if(file_exist_check(exp_home2) == 1) { file_unlink(exp_home2); } if(dll_ret == 1 && str_get_sub_n(dll_msg, 0, 20) == 'export_direct NODATA') { // 【防呆】明细行为0:DLL未生成任何文档,这里弹提示并终止,不打开任何文件。 exp_result_txt = '没有符合条件的订单明细数据,已终止导出(未生成文档)。'; if(m_lmt_summary != "") { exp_result_txt = exp_result_txt + ' 当前筛选:' + m_lmt_summary; } msg(exp_result_txt); } else if(dll_ret == 1 && str_get_sub_n(dll_msg, 0, 17) == 'export_direct OK ') { // 打开生成的文件:查询版始终打开;打印版仅在需要时打开 if(need_print == 1) { shell(exp_out); }shell(exp_out2); // 弹窗:含完整路径(msg可显示任意字符) if(need_print == 1) { exp_result_txt = '导出成功!' + dll_msg + ',脚本计时约 ' + m_tqsj + ' 秒。打印版=' + exp_out + ' 查询版=' + exp_out2; } else { exp_result_txt = '导出成功!' + dll_msg + ',脚本计时约 ' + m_tqsj + ' 秒。查询版=' + exp_out2; } msg(exp_result_txt); } else { exp_result_txt = '导出失败:ret=' + dll_ret + ' | ' + dll_msg; msg(exp_result_txt); } // ========================================== // 已取消向结果集写入提示语--导出信息仅用弹窗(msg)显示,网格不再展示内容。 // 但func_def_field声明了1列"结果",框架会渲染"最后一次db_run"的结果集;若不收尾, // 可能把前面某个字典小查询(如m_dw/m_chzt/m_nbwlbh)的结果误显示到网格。 // 这里用一条"返回0行"的空查询收尾(列数=1,与func_def_field一致),保证网格恒为空、干净。 // ========================================== db_run("SELECT N'' AS 结果 FROM (SELECT 1 AS n) AS t WHERE 1=0"); return 1; }; |
|
附件(登录后才可下载):2608141258_26.txt 说明: 原文件:ba_cosd_report_export_only_v62.txt 大小:48453 Byte
|

官方认证第三方服务团队 (越南-叶子) QQ:121305301 TEL:+84869350626
承接越南北部-河内周边省份-OIT部署和维护服务(24小时上门或远程服务)
服务介绍:http://www.onlyit.cn/onlyit_service.html
|
|
fwj3861 [个人空间] QQ名 越南-叶子(121305301)
    注册  2010-09-09 发贴数 641 精华贴 10 原创贴 20 来自 越南 状态 正常
级别 贵宾 |
|
fwj3861 [个人空间] QQ名 越南-叶子(121305301)
    注册  2010-09-09 发贴数 641 精华贴 10 原创贴 20 来自 越南 状态 正常
级别 贵宾 |
|
|
以上实战经历分享给有需要的同仁,希望大家都能在AI的帮助下做出更实用的自定义报表或功能需求 AI使用成本,MAX套餐:5小时(每月100美金),5小时重置一次 |
|
|
于 2026-08-14 13:07 被 fwj3861 修改

官方认证第三方服务团队 (越南-叶子) QQ:121305301 TEL:+84869350626
承接越南北部-河内周边省份-OIT部署和维护服务(24小时上门或远程服务)
服务介绍:http://www.onlyit.cn/onlyit_service.html
|
|
fwj3861 [个人空间] QQ名 越南-叶子(121305301)
    注册  2010-09-09 发贴数 641 精华贴 10 原创贴 20 来自 越南 状态 正常
级别 贵宾 |
|
|
附上AI自主更新的DLL编码规范文档 ERP 自定义报表 + DLL 导出开发规范 适用范围:onlyit(综合管理 oit)ERP 的自定义报表脚本(4GL)与配套导出 DLL(C++)。本规范由「客户订单出货明细/客户订单明细表」导出报表的实战踩坑沉淀而来,后续编写脚本和 DLL 均参照执行。 一、报表框架(4GL 脚本)硬性规则 1.1 七个钩子必须齐全。 func_can_run、func_def_lmt、func_init_lmt_val、func_def_field、func_def_imp、func_init、func_query 缺一不可。缺 func_def_imp 或 func_init 会表现为「没有检索菜单出来」或「点击查询没反应」。 1.2【重点.新坑】网格列数必须与 func_query 末尾最后一次 db_run 的结果列数一致。 否则框架报错: ERROR: dataset is not empty, records N field num M, set field num fail 原因:func_def_field 里 db_set_field_count(X) 声明了 X 列,而 func_query 结束时留下的数据集是 Y 列(Y≠X),框架拿 X 列的定义去套 Y 列/若干行的结果集,套不上就报「set field num fail」。 直连版(DLL 直接写 Excel)的正确做法:网格只作占位,不承载真实数据。func_def_field 用单列,func_query 末尾的占位 db_run 也只返回单列: int func_def_field() { db_set_field_count(1); db_set_field_info(0,'结果',"结果",600); return 1; }; // ... func_query 末尾: db_run("SELECT N'导出完成,请查看已自动打开的文档。' AS 结果"); 1.3 db_set_field_sum_flag('列名') 引用的列名必须真实存在于结果集,否则合计失败。 1.4 不能用独立 int main() 方式写报表逻辑;所有逻辑必须走上述框架钩子。 1.5 4GL 没有 file_read。需要 DLL 读文件时,由脚本 file_write 写参数文件、DLL 自己读。 二、大数据量导出的性能规则 2.1 禁止「每行多次 db_res() + 多次字符串拼接」。 4GL 字符串对象会累积堆积,上万行必崩 External exception EEFFACE。把字段拼接下沉到 SQL(SELECT 里 CAST 拼成一个 row_line 列),脚本每行只 db_res(0) 一次。 2.2 脚本里用 xls_ 原生函数逐格写(xls_write_cell)不可用于大数据。* 实测约 160 格/秒:232 行也要约 58 秒,上万行直接卡死客户端界面。此路已验证为死路。 2.3 最终方案:DLL 直连数据库(ODBC)。 DLL 内部取数 + 直接生成 xlsx(纯 ZIP/XML,不走 COM),39519 行约 7 秒(旧脚本方案约 5 分钟),提速约 60 倍。 三、编码(中文 + 越南文) 3.1 db_res() 返回 GBK(cp936)。非 GBK 字符(如越南文 Võ Nhật Mai)会退化成 0x3F 的 ?。两种修法: • 方案 A(脚本 + DLL,hex 传输):SQL 端 CONVERT(VARBINARY(MAX)→,2) 把整行转 UTF-16LE 十六进制,DLL 端 HexUtf16LeToUtf8 解码。 • 方案 B(直连 DLL,推荐):ODBC SQLGetData 用 SQL_C_WCHAR 原生宽字符取数,直接 UTF-16→UTF-8,无需 hex 传输。 3.2 DLL 回传给脚本的 ret 字符串,若含中文需转回 ANSI/GBK(SafeCopyRet 里 Utf8ToAnsi),否则脚本侧乱码。 四、序号 / 数据完整性 4.1 数据字段里的换行符(CR/LF/TAB)会把「整行拼成一列、再按换行分隔」的 blob 拆行,产生序号=0 的幻影行,导致序号重复/缺号。SQL 端对整行 REPLACE 掉 CR/LF/TAB。 4.2 ROW_NUMBER() 若 ORDER BY 的键不唯一,分页/多次执行排序不确定,会造成序号重复或缺失。ORDER BY 要用唯一稳定键(如 d.voucher_no, a.item_id)。 五、下拉 / 检索条件 5.1 def_lmt_dict 绑定的 SQL,两列不能同名。select eba_id, eba_id from eba 会导致下拉「有条件但不出数据/不显示编号」;必须给显示列起别名:select eba_id, eba_id as eba_disp from eba。 六、DLL 直连数据库(ODBC) 6.1 连接串(含密码)只放客户端本地配置文件(如 ...\dll\cosd_dbconn.ini),不硬编码进 DLL。用只读账号(db_datareader),严禁用 sa。 新建只读账号的 SQL(在 SQL Server 上用 sa 或管理员执行一次;rpt_export/S_ERP_2018/密码按现场改): -- 1) 服务器级:建登录。CHECK_POLICY=OFF 避免强密码策略导致后续报错 CREATE LOGIN rpt_export WITH PASSWORD = 'Rpt2018abc', CHECK_POLICY = OFF; -- 2) 数据库级:在业务库里建用户,只授予"只读"角色(db_datareader) USE S_ERP_2018; CREATE USER rpt_export FOR LOGIN rpt_export; ALTER ROLE db_datareader ADD MEMBER rpt_export; -- 3) 如需改密码(登录 28000 失败时常见于密码含特殊字符,改成简单字母数字) ALTER LOGIN rpt_export WITH PASSWORD = 'Rpt2018abc'; 该账号只有 db_datareader(全库只读),不能写/改/删,泄露风险可控,所以配置文件里明文放密码可接受。 6.2 驱动 {SQL Server}(旧版 DBNETLIB)兼容 SQL Server 2014。必设 SQL_ATTR_LOGIN_TIMEOUT / SQL_ATTR_QUERY_TIMEOUT,连不上要快速失败,避免冻死客户端。 6.3 客户端直连需在 SQL Server 端开 TCP 1433 + 防火墙放行(New-NetFirewallRule "SQL 1433")。注意 ERP 是三层架构(客户端→应用服务端 8888→本地 SQL),默认客户端不直连库;IP 以服务器在用网卡为准(现场 Embedded NIC 1 = 192.168.1.168 在用,NIC 2 = .138 网线未插)。 6.4 交叉编译 32 位 Windows DLL: i686-w64-mingw32-g++ -std=c++17 -O2 -shared -static -static-libgcc -static-libstdc++ \ -o cosd_direct_export.dll customer_order_ship_detail_export_v13.cpp -lodbc32 6.5【记录规矩.必须遵守】 编写 DLL 过程中,凡是为实现功能需要在服务器数据库上新建或运行的 SQL(建只读账号、建视图/函数/存储过程、加索引、开启 TCP/远程连接配置、授权 GRANT 等),都要把可直接运行的语句作为范例记进本规范对应章节,并注明:① 用途;② 执行位置(哪台服务器 / 哪个库);③ 执行账号(sa/管理员等);④ 注意事项。方便日后复用、换环境重建、以及现场排错。上面 6.1 的建账号 SQL 即为范例。 6.6【范例.加索引】JE 关联提速用的属性表索引。 用途:「客户订单出货明细」导出的 JE 关联多处对 ebs_vr_item_attr 做 WHERE attr_id='gdh'(建 #jebase、主查询 PIVOT、存在性校验),因 attr_id 无索引均为全表扫描;加此索引把它们变索引查找,加速扫描环节(注:FOR XML 分组拼接在小临时表上做,本索引帮不到,故为部分提速)。执行位置:业务库 S_ERP_2025(承载 ERP 数据的 SQL Server);执行账号:sa/DBA(只读账号 rpt_export 无建索引权限);注意:业务低峰执行,企业版可加 WITH (ONLINE=ON) 避免锁表,先查 attr_val 类型再二选一。 -- 先查 attr_val 类型:非 (n)varchar(max)/text 用方案A(带 INCLUDE),否则方案B -- SELECT DATA_TYPE, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS -- WHERE TABLE_NAME='ebs_vr_item_attr' AND COLUMN_NAME='attr_val'; -- 方案A(首选):覆盖 WHERE(attr_id)+JOIN键(voucher_id,item_id)+取值列 CREATE NONCLUSTERED INDEX IX_ebs_vr_item_attr_attrid ON dbo.ebs_vr_item_attr (attr_id, voucher_id, item_id) INCLUDE (attr_val); -- 方案B(attr_val 为 MAX/text 无法 INCLUDE 时):去掉 INCLUDE -- CREATE NONCLUSTERED INDEX IX_ebs_vr_item_attr_attrid -- ON dbo.ebs_vr_item_attr (attr_id, voucher_id, item_id); -- 回滚:DROP INDEX IX_ebs_vr_item_attr_attrid ON dbo.ebs_vr_item_attr; 配套认知(见 10.10 ④):若加索引后大范围导出仍慢,残余耗时在 SQL2014 的 FOR XML 分组拼接(每个工单号一次),已无库级手段可再快;只能改「一个工单取一条值」的 GROUP BY 写法(集合运算、无逐组拼接)换取速度,代价是极少数一单多机台的只显示一条。 6.7 多条连接串自动主备切换(内网/外网容灾)。 配置文件 cosd_dbconn.ini 可放多条 SERVER= 连接串,DLL 按文件顺序依次尝试,第一条连上即用、全部失败才报错。用途:客户端在内网走内网直连,在外网(远程/端口映射)时内网连不上自动切外网。约定:第 1 条=主(内网 LAN),第 2 条=备(外网 WAN);行首 # 或 // 为注释(DLL 跳过);连接串行必须含 SERVER=;注释用纯 ASCII 免编码问题;VERSION= 保持不变则不触发配置自更新覆盖本地改动。 VERSION=2026081301 # conn1 = primary (LAN) DRIVER={SQL Server};SERVER=192.168.1.168,1433;DATABASE=S_ERP_2018;UID=rpt_export;PWD=Rpt2018abc; # conn2 = backup (WAN, auto when LAN fails) DRIVER={SQL Server};SERVER=114.114.114.114,8888;DATABASE=S_ERP_2018;UID=rpt_export;PWD=Rpt2018abc; DLL 侧(≥314):ParseConfig 把所有含 SERVER= 的行收进列表,连接时 for 依次 SQLDriverConnect,每条登录超时设短(如 8 秒)让主连不上时尽快切备;全失败把每条错误都带回(CONNECT FAILED (tried N): [conn1]… ; [conn2]…)便于排查。注意:①外网每次会先等主(内网)超时再切备,有固定延迟(≈主超时秒数)+ 公网延迟,比内网慢是正常的;②把 DB 端口映射到公网有被扫描/爆破风险,只读账号损失可控,仍建议外网端口非 1433、路由只放行来源 IP、长期用 VPN 更稳;③判断跑的是不是新 DLL:314 起报错是 CONNECT FAILED (tried N) 新格式,老版是 CONNECT FAILED: 08001… 单条格式。 七、在线自更新(方便远程维护) 7.1 DLL 更新(需重启): 脚本内置 EXPECTED_DLL_VER,启动时调 DLL 的 get_dll_version 比对;不一致→confirm 提示→get_edoc_file 从文档管理下载→file_rename 覆盖→call_dll_func('gui_app','Gui_App_Restart') 重启客户端→return。 7.2 配置更新(静默): 脚本内置 EXPECTED_CFG_VER,调 DLL 的 get_config_version 比对;不一致→静默 get_edoc_file 下载覆盖,不弹窗。 7.3【易错点】文档管理里上传的 DLL/配置版本号,必须和脚本里的 EXPECTED 值一致。 否则其他客户端自更新会「下载→版本仍不符→再次提示」死循环。每次改 DLL,记得把文档管理里的 DLL 换成新版本、备注写对版本号。 八、交付规矩 所有报表相关文件产出即自动保存到默认目录 D:\6688-admin\cpp,无需每次提醒。 覆盖范围:DLL、.cpp 源码、报表脚本 .txt、配置 .ini,以及其它报表相关文档。该目录为现场 vncl-admin 机器的唯一默认目录(已连接、可读写),保存到这里即可,便于本地直接取用或上传 ERP 文档管理。 九、运行时"读模版"引擎 + 占位符规范 背景(为什么要这样做)。 早期 DLL 把模版布局(列宽、每格样式索引 s=、合计行位置、合并区)写死在代码里。但用户每次在 Excel 里重存模版,Excel 会重新编号 styles.xml 的样式索引、列宽/合并也可能变--写死的 DLL 就会错位,必须重新校准重编译。为彻底解耦,DLL 改为运行时直接读模版:模版怎么设计,输出就怎么来。 9.1 原理。 DLL 运行时用内置精简 inflate(仅解压的 RFC1951 实现;Excel 模版内部是 DEFLATE 压缩,而交叉编译器 i686-w64-mingw32 无 zlib,故内置一个只解压的 inflate,写回统一用 STORED 不需压缩器)解压模版的 xl/worksheets/sheet1.xml,再做字符串手术生成新 sheet。模版的其余一切原样保留:styles.xml、列宽 <cols>、workbook.xml(工作表名)、drawing/media(logo 图片)、printerSettings、以及用户加的条件格式(隔行底色)。 9.2 占位符约定(用户在模版单元格里摆好,DLL 按 token 文本替换,不依赖列头名称、不依赖列顺序): • - 明细数据列,k = SQL 结果的 0-based 字段序号。含 | D 的那一行 = 样板行/原型行,DLL 按它的样式逐条复制填数据。数字列(k ∈ {0,33,34,35,39} = 序号 / 订单数量 / 已出货 / 待出货 / 生产数量(Y))自动把单元格从内联字符串改写成真数字 <v>。列顺序不同的模版(如查询版把生产数量(Y)前移),靠给每格填不同的 k 解决,不会串列。
• [[Hk | - 条件回显,k = 0..21 对应 header22 的 h1..h22(仅打印版有)。 • / - 查询人 / 查询日期; - 四个合计(数字)。 • token 以内联字符串存,模版仍是合法 xlsx,用户能在 Excel 里正常打开编辑(会看到占位符文字)。 9.3 引擎四步。 ① 原型行之前=表头区,替换 ;② 原型行按记录数逐条克隆、替换 、行号顺延;③ 原型行之后=合计/尾部区,整体下移 (N-1) 行、替换 //;④ 修正 <dimension>、冻结窗格(ySplit=原型行-1)、<mergeCells>/<autoFilter>/<conditionalFormatting> 中大于原型行的行号顺延(隔行底色的范围自动跟到数据末尾)。 9.4 输出 zip。 除 sheet1.xml(重写,STORED)外所有条目原样拷贝;**丢弃 **calcChain.xml 并从 [Content_Types].xml / xl/_rels/workbook.xml.rels 移除其引用(新表无公式,留着悬空引用会让 Excel 弹"修复")。 9.5 维护方式(用户侧)。 用户直接在 Excel 里改这两个样板模版--列宽、字体、边框、底色、工作表名、移动占位符位置、用条件格式加隔行底色--只要不删占位符,重新上传覆盖即可,下次导出自动生效,无需重编 DLL。渲染由 Excel 按模版样式完成。 9.6 关键坑(务必记住)。 • 正则行号替换吞列字母:inflate 后做字符串手术,std::regex_replace 用 r="$1"+行号 时,若行号以数字开头(如 10),$1 与数字连成 $110 会被当成 $11(不存在的捕获组)→ 列字母被吞、r="A10" 变成 r="0",整表非法。修复=用中间标记(如 \x06)隔开 $1 与数字,再把标记换成行号。 • 数字占位符要改单元格结构:数字列不能只把文本 换成数字串(那样还是文本、合计/排序失效),要把整个单元格从 t="inlineStr"><is><t>…</t></is> 改写成 ><v>数字</v>。 • 工作表名:文件名带时间戳会超 31 字符上限且含非法字符;工作表名用固定基名(如 客户订单出货明细【打印版】/【查询版】)设进模版 workbook.xml 的 <sheet name=...>,DLL 原样保留即可。 • inflate 只需解压;写回全部用 STORED(不压缩),省掉 deflate 压缩器,DLL 无需链接 zlib。 • 数据里的 XML 非法字符必须清洗(现场踩过,会弹"修复"):ERP 老数据常含 XML 不接受的字节,写进单元格文本会让 sheet1.xml 非法,Excel 打开时弹"发现部分内容有问题,是否修复",修复时还会顺带删掉合并单元格。两类:① 控制字符 0x00–0x1F(除已在取数时 strip 掉的 \t \n \r 外,还有 0x0B/0x0C/0x1F 等);② 非法 UTF-8 序列--孤立代理项(如 0xD800,UTF-8 编成 ED A0 80)、0xFF/0xFE 之类非法字节、截断/过长的多字节序列(来自损坏的 Unicode)。openpyxl 等宽松解析器能读、Excel/xmllint 会严格报错(如 PCDATA invalid Char value 31、Char 0xD800 out of allowed range)。 • 清洗做法(统一在写单元格文本前做一次):逐字节遍历--<0x20 的控制字符换空格;对 ≥0x80 的按 UTF-8 规则校验整个多字节序列(前导字节定长度、续字节须 10xxxxxx、拒绝过长编码、码点在代理区 0xD800–0xDFFF 或 >0x10FFFF 或 0xFFFE/0xFFFF 一律丢弃换空格),校验通过才原样输出。这样有效的中文/越南文完整保留,只清坏字节。转义 & < > 也在这一步做。别只 strip <0x20--那样漏掉非法 UTF-8,现场会再次弹修复。 • $N+数字吞字符,不止行号,dimension 也中招(现场踩过,Excel 弹"修复 sheet1.xml 行2列0"):这是前面"正则行号替换吞列字母"的同类,但发生在 <dimension> 上。替换串 "<dimension ref=\"$11:$2"+行号 里 $1 后紧跟 1 被当成 $11、$2+行号被当成 $27(都不存在)→ dimension 变成非法的 ref=":4"。xmllint 只查良构、openpyxl 会自己重算 dimension,都放行;但 Excel 严格校验 dimension 格式就弹修复,还顺带删掉合并单元格。通用规则:任何 std::regex_replace 的替换串里 $N 后面绝不能紧跟数字--要么先 regex_search 把捕获组取成普通字符串再拼字面替换串(不含 $),要么用中间标记(如 \x06)隔开 $N 与数字。写完务必用 Excel(不是只 xmllint/openpyxl)实开一次。 • 参数文件别用换行分隔(字段可能含换行导致整体错位):脚本用 file_write 传参、DLL 按行 SplitLines 定位第 N 个字段时,只要某个字段(如条件回显 header22 里某个字典显示名)含了换行符,后面所有字段就整体错位--现场表现:两个模版路径被挤进主查询 SQL,SQL Server 把两个 D:\ 当成两个同名标签,报"标签 'D' 已声明,标签名称必须唯一"+语法错。修复=参数改用一个唯一分隔符(如 |~~PARAM~~|,别用 |~|--那个已被行数据占用)拼接,DLL 按同分隔符切,字段里有换行也不错位。DLL 可保留"找不到分隔符就回退按行切"以兼容旧脚本。失败时把解析到的字段+SQL写进 <输出>.paramdbg.txt 兜底,方便远程排错。 • 框架不渲染"无 FROM 的 SELECT"到网格:自定义报表用 db_run("SELECT N'提示语' AS 结果") 想把一行提示写进网格结果列时,ERP 框架对无 FROM 的 SELECT 不渲染(网格空)。修复=加一个派生表强制返回一行:db_run("SELECT N'…' AS 结果 FROM (SELECT 1 AS n) AS t")。(带 FROM 的正常查询能渲染,是对照。) 十、导出交互与临时文件约定(打印版/查询版二选一) 背景。 打印版(带标题+检索条件、定宽用于打印)使用频率低;查询版(无表头、宽列)是日常主力。所以默认只出查询版,需要时再勾打印版。同一份 DLL、同一次查询,按需生成一个或两个文件。 10.1 导出前弹"是否导出打印版"确认框(中越双语)。 func_query 取模版前先问:need_print = confirm('是否导出【打印版】? …[是]=打印版+查询版都导出,[否]=只导出查询版。 / Bản in ít dùng … [Có / 是]=… [Không / 否]=…')。confirm 约定:是返回 1、否返回 0(与七章自更新 relogin_choice 同一约定)。否时只出查询版,并省掉一次打印版模版下载与一次文件生成。 10.2 脚本里的越南文是否要用无声调 ASCII,取决于编辑器保存编码(实测修正)。 越南文声调字符(如 Bản、không、vấn 里的 ô ả ấ ầ ỉ 等)不在 GBK(cp936)内。若 ERP 报表编辑器/存储按 GBK 保存,遇到这些无法编码的字符会在该字符处把整条字符串截断,闭合引号 ' 被砍掉 → 引号数变奇数 → 加载报 there is odd "" or odd ' 引号没有匹配。特征极具迷惑性:源文件本身引号完全配平、无隐藏字符,报错也不给行号--因为坏的是 ERP 保存后的那份,不是你的源文件。「粘贴后当场运行能弹出带声调对话框」不能证明没问题:那是内存里的效果,经历一次保存+重载才暴露截断。这种编辑器上,脚本源码里(confirm/msg 一切字面量、乃至注释)的越南文一律用无声调 ASCII,例如:Ban in it dung, lan nay co can xuat [Ban in] khong? [Co]=xuat ca ban in va ban truy van; [Khong]=chi xuat ban truy van.。 【实测修正】但并非所有编辑器都截断--本客户端(onlyit 加密狗版 IDE)保存走 UTF-8,带声调可用。 实测:把 confirm 文案改成完整带声调越南文(Bản in ít dùng, lần này có cần xuất [Bản in] không? [Có / 是]=xuất cả bản in và bản truy vấn; [Không / 否]=chỉ xuất bản truy vấn.),保存 → 重新登录客户端(强制重载)→ 运行,无「引号没有匹配」、弹窗声调完整显示。所以带声调越南文在按 UTF-8 保存的编辑器上可用;只有按 GBK 保存的才必须退回 ASCII。唯一可靠的判定方法:改成带声调 → 保存 → 重登重载 → 运行;完整显示且不报错 = 可用;报「引号没有匹配」或声调变 ? = 退回无声调 ASCII。 别默认「一律 ASCII」,也别默认「一定能带声调」,以这个 save+reload 测试为准。注:数据里的越南文(客户名等)走 DLL 的 UTF-8 路径进 xlsx 由 Excel 显示,一直不受此限;本条只针对脚本源码。 10.3 DLL 按"路径是否为空"决定是否生成对应文件。 约定:脚本 need_print=0 时,把打印版的模版路径(参数 L[1])和输出路径(L[2])都置空字符串;DLL 侧 export_direct 对主表/纯净表分别 if(!tplPath.empty() && !outPath.empty()) 才生成,否则跳过。返回信息追加 full=0/1 plain=0/1 标识实际生成了哪些,便于核对。两版用同一套判断、互不影响,以后单独关掉任一版本都不必改结构。配套:脚本侧 shell() 打开、导出后 file_unlink 清理、弹窗文案都要按 need_print 分支--否分支不要 shell() 空的打印版路径、不要在提示里拼打印版路径。 10.4 导出与临时文件一律放 tmp 目录。 路径用 get_home_path() + "\tmp\...",不要放 db。tmp 在客户端重新登录时自动清空,避免导出文件在硬盘上无限累加。适用范围:两个 xlsx 输出、下载的模版副本(exp_home/exp_home2)、参数中间文件 cosd_direct_param.txt。必须用 get_home_path() 动态取客户端根目录,不要写死盘符/路径--不同客户端根目录不同(如 D:\6688-admin、D:\6687-YW-SL018),写死会跑错机器。 10.5 改 DLL 需重编上传时的版本联动纪律(复述强调)。 凡改了 DLL 需要重新编译的(本次为支持"跳过打印版"),必须四件事一起做:①DLL_VERSION 加一(如 direct-2026081309→...10);②报表脚本 EXPECTED_DLL_VER 同步改成同一版本;③把正确的新 DLL 上传到文档管理替换 cosd_direct_export.dll(务必选对文件--曾因上传错文件导致自更新反复弹窗、客户端重启循环);④单机可用设备直接替换 ...\dll\cosd_direct_export.dll,让本地版本=EXPECTED 免自更新弹窗,但多客户端仍需上传文档管理,否则别的机器版本不一致会循环下载旧的。 10.6 结果集/显示方案的字段绑定(坑)。 自定义报表加入报表分组后,网格列由"显示方案(列显示定义)"控制,它会残留旧脚本注册过的字段。要点:①结果集按字段标识绑定,func_def_field 里 db_set_field_info(0,'结果',...) 的第 1 参(标识)必须与 func_query 末尾 db_run("... AS 结果") 的列别名完全一致,否则绑不进去、列空(曾把版本标记误加到标识上导致列空,标记应放第 2 参"标题")。②显示方案里旧字段"隐藏≠删除",残留同名字段会让数据落到隐藏列;需在"列显示定义"里删掉旧字段,或把报表从分组移除后重新添加以按当前字段重建。③若不用结果集(只用弹窗),末尾用一条返回 0 行的空查询收尾:db_run("SELECT N'' AS 结果 FROM (SELECT 1 AS n) AS t WHERE 1=0"),防止把前面某个字典小查询的结果误显示到网格。 10.7 新增字典/报表类别后要刷新才生效(运维坑)。 在系统里新增"自定义报表类别"(或其它字典项)后,若客户端字典缓存未刷新,报表加载会报 自定义报表类别 X 未定义(如 类别 C 未定义, ds_006 客户订单明细导出)。这与脚本无关--重新登录客户端或刷新字典后即恢复。排查时先确认类别字典是否已刷新,别误往脚本/编码上找。 10.8 长 SQL/文案行必须拆短(ERP 编辑器粘贴坑)。 单条 sql+= "..." 或长文案字符串不要写成一整行超长(经验上控制在 ~100 字符内)。ERP 自定义报表编辑器在粘贴脚本时,对过长的行可能整行或尾部被截断,把 "..." 字符串拦腰截断,导致解析报 there is odd "" or odd ' 引号没有匹配。做法:SQL 一行一个字段/一小段、每行独立 sql+= "..."; 闭合;长文案(如中越双语 confirm)拆成 msg = '…'; msg = msg + '…'; 多段拼接。排查该错时先确认:①脚本引号是否真的成对(单双引号在本 4GL 里都是字符串定界符,' 与 " 各自要配对;串内的另一种引号是普通字符);②是否有超长行被粘贴截断;③粘贴是否完整(结尾有无 return 1; 和 };);④有无混入不可见字符(NBSP/零宽空格)。注意:自定义报表类别 X 未定义 是字典未刷新(见 10.7),与本条引号错误是两回事。 10.9 往模版插列的 XML 手术:必须处理自闭合空单元格,否则列序乱→Excel 修复(坑,实测)。 给带占位符的模版"在某列后插入 N 列"时(直接改 sheet1.xml、保 inlineStr,不用 openpyxl 以免占位符被转共享串),要把该列后所有单元格引用 r="…"、<cols>、mergeCell、dimension、autoFilter、spans 统一右移 N,再在锚点单元格后注入新单元格。关键坑:锚点若是自闭合空单元格 <c r="M3" s="6"/>(合计行常见),用 <c r="M3"[^>]*>.*?</c> 去定位会因为它没有 </c> 而一路吞到下一个 </c>,把新单元格插到错误位置,导致该行单元格不按列升序排列。xmllint/openpyxl 都能容忍,但 Excel 打开时判为损坏、弹"修复",并删除乱序单元格(表现为新列在数据行里变空)。修法:定位正则把自闭合分支放前面且不带 .*?:<c r="M{row}"(?:[^>]*/>|[^>]*>.*?</c>)。验证:生成后逐行检查单元格列号是否严格升序且唯一(这是 Excel 不弹修复的必要条件),别只靠 xmllint。 10.10 "按某键关联属性表并拼接多值"的慢查询提速(实测 52 秒 → 28 秒 → 4 秒)。 场景:主查询每行要按一个键(如工单号 gdh)去另一批表(ebs_vr_item_attr+ebs_vr_item+ebs_v,voucher_type='JE')取多条记录、去重逗号拼接成一格(SQL2014 无 STRING_AGG,只能用 FOR XML PATH('')+STUFF)。两步优化: ① 物化 + 索引(52→28)。 把 JE 三表关联结果先一次性写进带索引的临时表,后面只扫这张小表:SET NOCOUNT ON; IF OBJECT_ID('tempdb..#jebase') IS NOT NULL DROP TABLE #jebase; SELECT CONVERT(NVARCHAR(100),a2.attr_val) AS gdh_key, i2.ref_voucher_no, v2.voucher_date, i2.batch_no, i2.vr_item_ext_1 INTO #jebase FROM ebs_vr_item_attr a2 JOIN ebs_vr_item i2 ON … JOIN ebs_v v2 ON … AND v2.voucher_type='JE' WHERE a2.attr_id='gdh'; CREATE INDEX ix_jebase ON #jebase(gdh_key);。要点:键列用 NVARCHAR(100)(工单号很短)避免超 900 字节索引上限;只读账号 db_datareader 可以在 tempdb 建 #临时表(默认权限,无需写权限)。 ② 相关子查询只对结果行算(28→4,关键)。 不要写成 LEFT JOIN (SELECT DISTINCT gdh_key … 3个FOR XML … FROM (SELECT DISTINCT gdh_key FROM #jebase)) AS JE--那会对临时表里全部键(可能几万个)都算一遍拼接,而结果只用到其中几千行。改成把 3 个 STUFF((SELECT … FROM #jebase b WHERE b.gdh_key=T.gdh FOR XML …)) 直接放进主查询 SELECT 列表、按当前行的键 T.gdh 关联--这样只对结果行(本例 2344)算,#jebase 按键有索引即索引查找。这一步是数量级提速。 ③ DLL 配合多语句批处理。 上面是"SET NOCOUNT ON + SELECT INTO + CREATE INDEX + 主 SELECT"的批处理,前几条无结果集。DLL 执行后要跳过无列的结果集再取主查询:SQLNumResultCols 若为 0 则 while(ncols==0 && guard<50){ SQLMoreResults; SQLNumResultCols; }(NOCOUNT ON 下前置语句不产生结果集,通常一次就定位到主查询;guard 防死循环)。改 DLL 记得走 10.5 的版本联动(本例 313)。 ④ 仍慢的下一手。 若上面做完主要耗时落在"建 #jebase 的一次性扫描"(扫 ebs_vr_item_attr 的 attr_id='gdh' 全表),再快需在 ebs_vr_item_attr(attr_id) 上建库级索引--只读账号不能建,须 DBA/sa 执行,SQL 按 6.5 记档。 10.11 大范围导出的最终定论:FOR XML** 分组拼接是 O(工单号数),库级无法再快,改 GROUP BY 取一条(实测)。** 10.10 的「临时表物化 + 相关子查询只对结果行算」对小/中范围很快(当天 2344 条 4 秒);但大范围(5 个月 39519 条)每个不同工单号都要做一次分组拼接:实测预聚合(#jeagg,每工单号 FOR XML 一次)与相关子查询(每结果行一次)耗时几乎相同(≈120 秒),说明每个订单的工单号基本唯一、无重复可省,瓶颈 100% 在 SQL2014 的 FOR XML 分组拼接本身。库级索引(6.6)只加速扫描、碰不到拼接,实测无效(120→120)。终局改「每工单号取一条上机记录」:SELECT DISTINCT … INTO #jebase → WITH ranked AS (SELECT …, ROW_NUMBER() OVER(PARTITION BY gdh_key ORDER BY voucher_date DESC)AS rn, COUNT(*) OVER(PARTITION BY gdh_key) AS cnt FROM #jebase) SELECT rn=1 那条 …,值末尾 + CASE WHEN cnt>1 THEN N' 等' ELSE N'' END(标记该工单号还有多条),INTO #jeagg 建索引,主查询简单 LEFT JOIN #jeagg。集合运算、无逐组拼接,大范围回到 ~10 秒级。取三列取同一条记录(按上机日期最新那条),避免张冠李戴;代价:极少数「一单多机台/多次上机」的只显示最新一条 + 「 等」。教训:SQL2014 里对每个分组做 FOR XML 字符串拼接,成本随分组数线性增长且常数大,大数据量场景优先考虑「取一条 + 标记多条」而非全量拼接。 十一、大数据量导出「116 秒 → 9 秒」三段式根因(实测定档) 背景。 客户订单出货明细【查询版】5 个月 39519 行导出,外网经 VPN 实测 116 秒。给 DLL 返回信息加内部分段计时(fetch_ms/gen_ms/v=版本)后,定位到三个互相独立的瓶颈,逐一击破到 9 秒(fetch 4.2s + gen 5.3s)。方法论:先把总耗时拆成「查询 / 取数 / 生成」三段分别计时,再对症下药,不要凭感觉猜--本次前几次改动「没变化」其实混着「旧 DLL 没更新到」和「改错了地方」两类假象,全靠分段计时和版本号打点才理清。 11.1 PIVOT 派生列被二次 JOIN 会打断谓词下推(查询侧)。 主查询用一个「对整张 ebs_vr_item_attr 做 PIVOT」的派生表 T 取工单号等列。给 T 追加 LEFT JOIN #jeagg ON JEA.gdh_key=T.gdh 后,T.gdh 被二次消费,优化器不再把 BA 日期过滤下推进 PIVOT,导致整张属性表被全表透视 + LOB 转换。现象极具迷惑性:同一 SQL 在 SSMS 里 1.5 秒、经程序却 116 秒,且产出只有 135 行。修法:把基表(不含 JE 列、不挂 #jeagg)先 SELECT … INTO #base(此时下推正常,~1.3 秒),再用 3.9 万行的 #base 去 LEFT JOIN #jeagg(135 行)--把「会打断下推的那步」隔离出去。 SSMS 单跑三段(#jebase/#jeagg、#base、join)分别 <1s / 1.3s / 0.04s 验证。 11.2 「SSMS 快、程序慢」先排 ARITHABORT,但本例不是它。 老 {SQL Server} ODBC 驱动默认 SET ARITHABORT OFF、SSMS 默认 ON,同一 SQL 会因此编译出两套计划,是该类问题的经典头号嫌疑。本例实测在 SSMS 里 ON/OFF 都 1.3 秒 → 排除。**教训:遇到这现象先在 SSMS 里 SET ARITHABORT OFF 复跑验证,别直接改;真是它就在批处理最前加 SET ARITHABORT ON。 11.3 多语句 + 临时表批处理会让老驱动退化成「服务器游标」、逐行网络往返(取数侧,这才是 116 秒里最先暴露的一段)。 DLL 用 SQLExecDirect(整段多语句批处理) + SQLFetch 逐行取。多语句 + 临时表会让老 {SQL Server} 驱动对最终结果集走「服务器游标」,退化成一行一次网络往返:内网每行 ~2ms(×3.9 万 ≈ 81s)、经 VPN 每行 ~2.9ms(×3.9 万 ≈ 115s)--LAN/VPN 差值随每行 RTT 线性缩放,即铁证。不带 JE 的老版是单条 SELECT,驱动走 firehose 整包传输,故 7 秒。修法:在 SQL 里插注释标记 /*__EXECSPLIT__*/ 分界,DLL 见标记就把「建临时表(无结果集)」与「最终 SELECT」拆成两次 SQLExecDirect;临时表是连接级作用域、跨两次执行不丢;单条 SELECT 走 firehose。 效果:取数 115 秒 → 3~4 秒。标记是 SQL 注释,新旧 DLL/脚本任意搭配都不出错(向后兼容)。辅助判定:让 DLL 弹窗分别打 fetch_ms(连接 + 执行 + 取回)与 gen_ms(生成 xlsx);也可在导出进行中查 sys.dm_exec_requests,wait_type** 反复为 ASYNC_NETWORK_IO 即「服务器算完了在等客户端取数」,实锤客户端/网络侧。** 11.4 运行时模版引擎逐行 std::regex 编译是隐形大头(生成侧,gen 112 秒 → 5 秒)。 EngGenerate 在几万行的循环内,EngRenumber(每行 2 个)+ EngNumCell(每数字列 2 个)都用 std::regex_replace(o, std::regex("…"), …)--std::regex(...) 构造即编译,3.9 万行 × ~12 次 ≈ 近 50 万次正则编译,实测 112 秒。修法:把这两个热点函数的正则全换成纯字符串操作(手工扫描 r="<列字母><行号>" 逐个比对行号;rfind("<c ") / find("</c>") 定位单元格、取 r=/s= 原样拼回)。EngTextTok 本就是 find/replace,不用动。上线前用小程序拿新旧两版对同一样板行比对输出完全一致再发(行号单/多位、A/AA/AQ 多列、数值/负/空/非数字、有无 s= 全覆盖)。通则:任何在几万行循环里跑的地方,禁止 std::regex(构造即编译,极慢);要么预编译成 static const std::regex 提到循环外,要么直接纯字符串。 11.5 定位手法沉淀:分段计时 + 版本号打进返回消息。 把 DLL 返回扩成 export_direct OK rows=… elapsed_ms=… fetch_ms=… gen_ms=… v=<DLL版本> …(保持前 17 字符 export_direct OK 不变,脚本判成功逻辑不受影响)。v= 直接消除「到底加载的是不是新 DLL」的反复扯皮(本次多次「没变化」正是旧 DLL 没更新到);fetch_ms/gen_ms 一眼分清取数 vs 生成。凡「改了没效果」先确认 v= 是不是目标版本,再看分段耗时,别再猜。 关联外网访问:大数据量导出务必在外网真机(VPN)上验证,内网(尤其数据库本机 RTT≈0)测不出逐行往返那段的真实代价。 十二、外网接入(VPN)与「连上 VPN 后微信/上网断了」的处理 背景。 外网电脑要访问内网 ERP 数据库,不走公网映射 SQL 端口(不安全),改由路由器自带 L2TP/IPSec VPN 拨入内网;拨通后配置文件 cosd_dbconn.ini 走内网直连(如 192.168.1.168,1433)。本章记录客户端拨号与最常见的「连上 VPN 后微信、网页都上不了网」的处理。 12.1 服务器端(路由器,以 TP-LINK ER6220T 为例)四件套。 ①L2TP 服务器:服务接口选 WAN、开启 IPSec 加密、设预共享密钥 PSK;②IP 地址池(如 192.168.60.2-192.168.60.20);③VPN 账号(类型 L2TP、本地地址 192.168.60.1、绑地址池、DNS 填内网 DNS 192.168.1.1、最大会话数按人数);④公网 IP 固定或用 DDNS。 12.2 客户端拨号 + 必加的 NAT 穿透注册表项。 Windows 建「使用预共享密钥的 L2TP/IPSec」连接,填公网 IP、PSK、账号密码。L2TP 服务器在路由器后经过 NAT,Windows 默认不允许,必须加注册表并重启:reg add HKLM\SYSTEM\CurrentControlSet\Services\PolicyAgent /v AssumeUDPEncapsulationContextOnSendRule /t REG_DWORD /d 2 /f。不加常报 789/809。 12.3 【核心坑】连上 VPN 后微信/网页都上不了网 = 全隧道副作用,改分隧道。 Windows L2TP 默认勾「在远程网络上使用默认网关」(全隧道),连上后所有流量(含微信、上网)都被塞进隧道绕到内网,而内网这条路出不了公网,于是微信显示「当前网络不可用」。修法:改成分隧道--只让去内网网段的流量走 VPN,其余照走本地宽带。 最干净是管理员 PowerShell 两条命令(会随连/断自动管理路由,连断都不用手动加): # ERP-VPN 若是"对所有用户"的共享连接(常见),命令必须加 -AllUserConnection,否则静默不生效(见 12.6) Set-VpnConnection -Name "2018-VPN" -SplitTunneling $True -AllUserConnection Add-VpnConnectionRoute -ConnectionName "2018-VPN" -DestinationPrefix "192.168.1.0/24" -AllUserConnection # 验证:应显示 SplitTunneling : True Get-VpnConnection -AllUserConnection -Name "2018-VPN" | Format-List Name, SplitTunneling, Routes 第一句关掉「用默认网关」(等同分隧道);第二句只把去 192.168.1.x 内网网段(数据库 192.168.1.168、内网 DNS 192.168.1.1 都在内)的流量走 VPN。改完重连:上网归本地、连库归 VPN,两不误。关键提醒:VPN 拿到的是地址池 192.168.60.x,而数据库在 192.168.1.x,是不同网段--所以分隧道后必须显式加 192.168.1.0/24 这条路由,否则连库也会不通(全隧道时是因默认路由全走 VPN 才「碰巧」能连)。 若内网还有其它网段要访问,再补对应 Add-VpnConnectionRoute。这两条命令每台外网电脑各跑一次即长期有效,纳入外网用户接入标准步骤。也可在连接属性→网络→IPv4→高级里手动取消「在远程网络上使用默认网关」,但仍需另加内网网段路由,不如上面两条省事。 12.4 【坑】VPN 服务器公网 IP 与你的 RDP 跳板是同一个 IP 时的回环。 若你是通过 RDP 远程到某台机器上操作,而该 RDP 的公网 IP 与 L2TP VPN 服务器同一个公网 IP:在这台机器上拨全隧道 VPN 会把维持 RDP 的流量也塞进隧道、形成回环,轻则拨不上、重则拨通瞬间掉线。判断:先在该机 ping 192.168.1.168--能 ping 通说明它本就在内网,根本不需要 VPN(VPN 是给真正在外网、连不进内网的机器用的);真需要则用 12.3 分隧道避免掐断 RDP。验证 VPN 一定要用「真外网」机器(如笔记本连手机热点),内网机(尤其数据库本机)测不出真实效果。 12.5 验证与安全收尾。 拨通后三查:①微信/网页正常(公网回来了);②ping 192.168.1.236 通,且 Test-NetConnection 192.168.1.168 -Port 1433 的 TcpTestSucceeded=True(内网 1433 端口可达);③ipconfig /all 里 VPN 适配器 IPv4 是地址池内的 192.168.60.x、DNS 为下发的公网 DNS(见 12.6,越南用 8.8.8.8)。三项过即整链路通(外网→VPN→内网库)。确认可用后,把原来暴露 SQL 的公网端口映射删除,外网统一走 VPN--只读账号 + VPN 双重收口,SQL 不再裸露公网。 12.6 【实战定论】「连 VPN 后微信/上网上不了网」的完整排查与两个致命坑。 这段最容易反复绕,把定论固化下来。先明确:微信上不了网基本是「全隧道 + DNS」两件事,网络没断。 坑一(绕最久):共享(对所有用户)VPN 连接改分隧道,命令必须加 -AllUserConnection,否则静默不生效。 若 VPN 连接建成「允许其他人使用此连接」(对所有用户),它存在系统级电话簿里;而 Set-VpnConnection/Get-VpnConnection 默认只找当前用户电话簿,于是命令要么报 VPN connection ... was not found(错误 623「找不到电话簿项目」)、要么静默什么都没改--分隧道从没真正加到你在用的连接上,它一直是全隧道、把整机网络闷死,且「重启后又失效」的假象也源于此(其实从没生效过)。判据:直接跑 Get-VpnConnection -Name "2018-VPN" 报 623 找不到 = 它就是共享连接。 修法:12.3 的命令全部加 -AllUserConnection;用 Get-VpnConnection -AllUserConnection -Name "2018-VPN" 能查到、且 SplitTunneling : True 才算真设上,然后断开重连生效。设在共享(系统级)连接上还有个附带好处:重启不丢,一劳永逸。 坑二:DNS 要按所在地区选,别用连不通的。 越南现场曾把 DNS 填成中国大陆的 114.114.114.114--该地址在越南不可达,造成「路由已通、但域名全解析不了」,表现同样是「上不了网」。改用全球可达的 8.8.8.8(Google)/1.1.1.1(Cloudflare);路由器 VPN 账号下发的 DNS 也一并改成 8.8.8.8(连库走 IP、不需要内网 DNS),改完点路由器「保存配置」持久化。 定论式排查顺序(照做即定位): 先跑 ping 8.8.8.8--它全球可达且会回应 ping,是理想的「纯路由」探针(别用 114,它可能不回 ICMP,会误判成路由不通)。①ping 不通 → 分隧道没生效,九成是坑一(没加 -AllUserConnection),按 12.3 带 -AllUserConnection 重设并重连;②ping 通、但域名打不开 → 是坑二 DNS,把 DNS 换成 8.8.8.8/1.1.1.1;③都正常 → 收工。全程判「有没有修好」只看微信/网页能否使用,别看网络和共享中心里 VPN 那条「无法连接到 Internet」标签--那只是 Windows 拿探测地址走 VPN 探测失败的装饰性提示,分隧道下它常年这么显示,不影响使用。 十三、运行时模版引擎的健壮性实战坑(v318–v320) 背景。 这几处都是「改了引擎/模版后才暴露、且只在特定模版上炸」的坑,逐一定档。总纲:模版引擎是纯字符串/XML 拼接,改完必须端到端验证(见 13.5)再发。 13.1 明细行为 0 时防呆终止,不生成空文档(v318)。 DLL 取数后 N==0 → 不调模版引擎、不生成 xlsx,返回以 export_direct NODATA 开头的串;脚本按前 20 字符识别该分支,弹「没有符合条件的数据,已终止导出」,不 shell 打开、不留空文件。与成功分支(判前 17 字符 export_direct OK )互不冲突(NOD≠OK)。 13.2 【核心坑】用 Excel 编辑模版后占位符被转共享字符串,引擎须在加载时自动内联(v319)。 引擎靠在 sheet1.xml 里找 | D 文本定位原型行。但只要用户用 Excel 打开模版、编辑(加底色/条件格式/样式)、保存,Excel 就把 [[Dk | 等占位符从内联字符串(inlineStr)转成共享字符串(sharedStrings),sheet1.xml 里不再有 | → 报 prototype row not found。修法:CloneZipTemplate 解压 sheet1.xml 后、EngGenerate 前,解析 xl/sharedStrings.xml,把 sheet1.xml 中引用「含 [[ 的共享串」的单元格 <c t="s"><v>N</v></c> 就地转回内联(EngInlineSharedPlaceholders)。 非占位符共享串(表头/公司名)保持不动、sharedStrings.xml 原样保留。这样用户可自由用 Excel 改模版存盘,无需再手工修复(替代了早期「手工把占位符转回内联」的临时办法)。
13.3 【踩了一整轮的深坑】C++ std::regex 的 . 不匹配换行,解析共享字符串/拆行必须用 [\s\S](等效 DOTALL)(v320)。 Excel 的 sharedStrings.xml 会把部分 <si> 格式化跨行(含换行符)。用 <si>(.*?)</si> 解析时 . 不吃换行 → 跨行的 <si> 被整条漏读 → 后续索引整体前移(实测漏 3 条:uniqueCount=95 却只解析出 92)→ 内联时占位符张冠李戴(表头 N1=指令单号(索引43)被错当 [[D0 | (索引46))→ 引擎把表头行误当原型克隆,数据行全变表头文字、真原型行留在底部,Excel 打开报「修复 sheet1.xml 视图」、文件损坏。判据:sharedStrings 的 uniqueCount 属性 ≠ 你解析出的条目数,就是漏读了跨行 <si>。修法:EngParseSST 的 <si>/<t> 正则、EngSplit 的 <row> 正则,.*? 一律改 [\s\S]*?。 通则:凡在 XML/多行文本上用 std::regex 且模式含 . 又可能跨行的,一律用 [\s\S] 代替 .(std::regex 的 ECMAScript 无 DOTALL 开关)。 13.4 隔行底色(偶数行)条件格式,克隆数据行后须把 sqref 扩到全部数据行。 隔行底色是「偶数行加底色」(MOD(ROW(),2) 之类表达式),模版里 <conditionalFormatting sqref="A2:AQ2"> 只圈了原型行一行。引擎克隆数据行时若不扩 sqref,只有第一行有底色。修法(EngFixCondFormatSqref):把含原型行的 sqref 范围底部扩到 lastData(A2:AQ2 → A2:AQ末行);整体在原型行下方的 sqref 整体下移;支持多段(空格分隔)。 注意:①通用的合并/筛选 ref= 处理要用 \bref= 排除 sqref(否则 sqref 被当 merge、只下移不扩底);②conditionalFormatting 在 </sheetData> 之后(post 段);sheetView 里的选中区 sqref 在 pre 段、不要动。 13.5 改模版引擎必须端到端验证再发(硬性)。 引擎极易在边界(内联/共享串、换行、单元格样式)出错且只在特定模版暴露(典型:「打印版 OK、查询版坏」)。改完必做:①用真实模版(尤其被 Excel 编辑过、含共享字符串+换行的那种)跑一遍生成;②xmllint --noout 校验生成的 sheet1.xml 合法;③检查:数据行是真数据、无 残留、无表头文字混入数据行、原型行被正确消费(不残留在底部)、条件格式 sqref 扩到末行;④DLL 版本号必加一--哪怕上一版也是当天日期,只要现场已装过(哪怕装的是坏的),不加一客户端版本检查就不会重新下载修好的(本轮 v319 装坏后必须跳 v320 才生效)。 |
|
附件(登录后才可下载):2608141322_30.docx 说明: 原文件:DLL编写规范_公开版.docx 大小:52182 Byte
|

官方认证第三方服务团队 (越南-叶子) QQ:121305301 TEL:+84869350626
承接越南北部-河内周边省份-OIT部署和维护服务(24小时上门或远程服务)
服务介绍:http://www.onlyit.cn/onlyit_service.html
|
|
|
|