原创_AI编写提取订单明细和出货数量,上机信息的自定义报表脚本,DLL文件(39519条10秒完成)_使用经验
学习.交流
专业源于专注
www. onlyit. cn   
学习交流 文件下载 手册资料 交流QQ群

    

 5  1/1   1  
作者
内容
fwj3861  [个人空间]
QQ名  越南-叶子(121305301)


注册  2010-09-09
发贴数  641
精华贴  10
原创贴  20
来自  越南
状态  正常

级别  贵宾
#1»发布于2026-08-14 13:28

由于客户订单明细比较多,从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。 
// 占位符: 
Dk
=第k个SQL字段(数据行,标记原型行); 
Hk
=第k个条件回显; 
//         
FP
=查询人 
FD
=查询日期 
FS0..3
=四个合计。 
// 保留模版一切(列宽/样式/表名/页面/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之前):替换 
Hk
 
    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, "
H"+std::to_string(h)+"
", header[h]); 
        body+=r; 
    } 
    // 数据行:克隆原型,逐条替换 
Dk
 
    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, "
D"+std::to_string(k)+"
", 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, "
D"+std::to_string(k)+"
", v); 
        } 
        body+=r; 
    } 
    // 合计/尾部行(pi之后):下移 + 替换 
FP
/
FD
/
FSk
 
    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, "
FS"+std::to_string(s)+"
", v); 
        } 
        r=EngTextTok(r, "
FP
", totals.size()>0?totals[0]:""); 
        r=EngTextTok(r, "
FD
", 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
来自  越南
状态  正常

级别  贵宾
#2»发布于2026-08-14 12:58

再发报表脚本文件》》》》》》 
// 文件名: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),查询版模版放 
D40
/
D41
/
D42
。 
// 历史版本与踩坑详见 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 与模版 
D0
..
D42
 均不受影响。 
    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
来自  越南
状态  正常

级别  贵宾
#3»发布于2026-08-14 12:59

最后是执行时效提示








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

fwj3861  [个人空间]
QQ名  越南-叶子(121305301)


注册  2010-09-09
发贴数  641
精华贴  10
原创贴  20
来自  越南
状态  正常

级别  贵宾
#4»发布于2026-08-14 13:07

以上实战经历分享给有需要的同仁,希望大家都能在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
来自  越南
状态  正常

级别  贵宾
#5»发布于2026-08-14 13:22

附上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 文本替换,不依赖列头名称、不依赖列顺序): 
•  
Dk
 - 明细数据列,k = SQL 结果的 0-based 字段序号。含 
D 的那一行 = 样板行/原型行,DLL 按它的样式逐条复制填数据。数字列(k ∈ {0,33,34,35,39} = 序号 / 订单数量 / 已出货 / 待出货 / 生产数量(Y))自动把单元格从内联字符串改写成真数字 <v>。列顺序不同的模版(如查询版把生产数量(Y)前移),靠给每格填不同的 k 解决,不会串列。 • [[Hk
 - 条件回显,k = 0..21 对应 header22 的 h1..h22(仅打印版有)。 
•  
FP
 / 
FD
 - 查询人 / 查询日期;
FS0..3
 - 四个合计(数字)。 
•  token 以内联字符串存,模版仍是合法 xlsx,用户能在 Excel 里正常打开编辑(会看到占位符文字)。 
9.3 引擎四步。 ① 原型行之前=表头区,替换 
Hk
;② 原型行按记录数逐条克隆、替换 
Dk
、行号顺延;③ 原型行之后=合计/尾部区,整体下移 (N-1) 行、替换 
FP
/
FD
/
FSk
;④ 修正 <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 与数字,再把标记换成行号。 
•  数字占位符要改单元格结构:数字列不能只把文本 
Dk
 换成数字串(那样还是文本、合计/排序失效),要把整个单元格从 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


 5  1/1   1  

登录后方可发贴


[ 电话: 0571-85462761 王先生 QQ: 124520435 加入软件QQ群 - 杭州 - 浙ICP备19051128号-1 网安 33010402003225 ]