Code Forensics · Static Analysis 程式碼鑑識 · 靜態分析 代码取证 · 静态分析

Two legacy systems,
one pair of hands
兩套舊系統,
同一雙手
两套旧系统,
同一双手

A forensic comparison of the Water Supplies Department (WSDSCU) and the ICAC (icaccu30) legacy Classic ASP systems. The question: were they written by the same author? 對水務署(WSDSCU)與廉政公署(icaccu30)兩套 Classic ASP 舊系統的鑑識比對。問題是:它們是否出自同一位作者之手? 对水务署(WSDSCU)与廉政公署(icaccu30)两套 Classic ASP 旧系统的取证比对。问题是:它们是否出自同一位作者之手?

Verdict: Same origin · Confidence ≈ 95% 判定: 同源 · 置信度約 95% 判定: 同源 · 置信度约 95%
en/loan.asp · both repos
1234
For Each Field in Request.Form
    TheString = Field & "= Request.Form(""" & Field & """)"
    Execute(TheString)
Next

The smoking gun — this rare, dangerous auto-variable idiom appears in 48 WSDSCU files and 53 ICAC files. It is no ASP textbook pattern; it is one person's personal habit. 鐵證——這個罕見而危險的自動變數寫法,出現在水務署 48 個檔案、廉政公署 53 個檔案中。它不是任何 ASP 教程的通用寫法,而是同一個人的個人習慣。 铁证——这个罕见而危险的自动变量写法,出现在水务署 48 个文件、廉政公署 53 个文件中。它不是任何 ASP 教程的通用写法,而是同一个人的个人习惯。

These are not two systems that merely look alike. They are the same base codebase, by the same author — one credit-union management product, customized for two clients and then maintained apart. 這並不是兩套「碰巧相似」的系統,而是同一位作者、基於同一套底層程式碼做出來的兩個客戶版本——同一套儲蓄互助社管理軟體,分別定制給兩個客戶後各自維護。 这并不是两套「碰巧相似」的系统,而是同一位作者、基于同一套底层代码做出来的两个客户版本——同一套储蓄互助社管理软件,分别定制给两个客户后各自维护。

The Evidence核心證據核心证据

Six pieces that settle it 六項定案證據 六项定案证据

Each item below is, on its own, hard to explain by coincidence. Together they are conclusive. 下列每一項單獨來看都難以用「巧合」解釋;合在一起則是鐵證。 下列每一项单独来看都难以用「巧合」解释;合在一起则是铁证。

01

Character-identical infrastructure 基礎設施逐字元相同 基础设施逐字符相同

Both roots share 8 identically-named files (conn.asp, CheckUserStatus.asp, illegal.asp, logout.asp, timeout.asp, index.asp, addUserLog.asp, main.css). The anti-framing referer check is word-for-word identical, comments and all. 兩邊根目錄共有 8 個同名檔案conn.aspCheckUserStatus.aspillegal.asplogout.asptimeout.aspindex.aspaddUserLog.aspmain.css)。其中防盜鏈 referer 校驗邏輯連註解都一字不差。 两边根目录共有 8 个同名文件conn.aspCheckUserStatus.aspillegal.asplogout.asptimeout.aspindex.aspaddUserLog.aspmain.css)。其中防盗链 referer 校验逻辑连注释都一字不差。

CheckUserStatus.asp
1234567
http_referer = lcase(Request.ServerVariables("HTTP_REFERER"))
http_host = lcase(Request.ServerVariables("HTTP_Host"))
If http_referer = "" then
    response.redirect "../illegal.asp"
elseif instr(1,http_referer,"http://" & http_host) <> 1 then
    response.redirect "../illegal.asp"
end if
02

A rare, dangerous fingerprint 罕見而危險的指紋 罕见而危险的指纹

The author dynamically rewrites every form field into a VBScript variable and Executes it — an unusual anti-pattern that is also a security hole. It appears across 48 / 53 files in the two systems. This single trait nearly closes the case. 作者把每個表單欄位動態拼成 VBScript 變數再 Execute 執行——這是一種罕見的反模式,同時也是安全漏洞。它在兩套系統中分別出現於 48 / 53 個檔案。單憑這一條幾乎就能定案。 作者把每个表单字段动态拼成 VBScript 变量再 Execute 执行——这是一种罕见的反模式,同时也是安全漏洞。它在两套系统中分别出现于 48 / 53 个文件。单凭这一条几乎就能定案。

en/*.asp · Execute() in 48 / 53 files
1234
For Each Field in Request.Form
    TheString = Field & "= Request.Form(""" & Field & """)"
    Execute(TheString)
Next
03

The same Hong Kong fiscal config 相同的香港財政年度設定 相同的香港财年设定

Both conn.asp files open the connection the same way and carry the same idiosyncratic FMonthStart = 8 (August fiscal-year start, a Hong Kong tell) and an identical month array. 兩邊 conn.asp 以相同方式開啟連線,並帶有同樣特殊的 FMonthStart = 8(以 8 月為財政年度起點,香港特徵)與一模一樣的月份陣列。 两边 conn.asp 以相同方式打开连接,并带有同样特殊的 FMonthStart = 8(以 8 月为财年起点,香港特征)与一模一样的月份数组。

conn.asp
1234
Set conn = Server.CreateObject("adodb.connection")
FMonthStart = 8
ArrMonth = Array("Jan","Feb","Mar","Apr","May","Jun", _
                "Jul","Aug","Sep","Oct","Nov","Dec")
04

Same product, same version scheme 同一產品、同一版本號體系 同一产品、同一版本号体系

Both footers print the same product tagline and the same Ver X.XX.YYYYMM version format — two builds of one commercial product. 兩邊頁腳印著相同的產品標語與相同的 Ver X.XX.YYYYMM 版本格式——同一商業產品的兩個版本。 两边页脚印着相同的产品标语与相同的 Ver X.XX.YYYYMM 版本格式——同一商业产品的两个版本。

index / logout footer
12345
' WSDSCU"Credit Union Ver 1.01.200604"
' ICAC  "Credur Union Ver 2.00.200804""VER3.0i"

' shared tagline, both systems:
"… Credit Union — Membership, Accounting, Savings and Loans Software"
05

Identical financial engine 完全相同的金融函式庫 完全相同的金融函数库

The loan-payment PMT() function and the fiscal-year helpers in init.asp are character-for-character identical — same formula, same edge cases. Even the adversarial reviewer confirmed this one. 還款額 PMT() 函式與 init.asp 中的財政年度輔助函式逐字元相同——公式相同、邊界處理相同。連「唱反調」的覆核員都確認了這一條。 还款额 PMT() 函数与 init.asp 中的财年辅助函数逐字符相同——公式相同、边界处理相同。连「唱反调」的复核员都确认了这一条。

en/init.asp
123456
function PMT(ByVal Rate, nPer, PV, FV, iType)
  PMT = (-PV*(1+Rate)^nPer - FV)*Rate _
        /((1+Rate*iType)*((1+Rate)^nPer-1))
end function

Function GetFiscalYear(ByVal x)   ' FMonthStart drives this
06

Same database, down to a sub-query 同一套資料庫,連子查詢都一樣 同一套数据库,连子查询都一样

Table and column names are shared (loginUser, userLog, loanApp, memmaster, Share; memno, memname, membday…). The birthday-list report runs the exact same SQL, including its DISTINCT sub-query. 表名與欄位名共用(loginUseruserLogloanAppmemmasterSharememnomemnamemembday…)。生日名單報表跑的是完全相同的 SQL,連 DISTINCT 子查詢都一樣。 表名与字段名共用(loginUseruserLogloanAppmemmasterSharememnomemnamemembday…)。生日名单报表跑的是完全相同的 SQL,连 DISTINCT 子查询都一样。

birthdayListPrint.asp
12345
select m.memno, m.memname, m.memcname,
       CONVERT(varchar, m.membday, 107) as membday
from   memmaster m,
       (SELECT DISTINCT memNo FROM Share WHERE amount > 0) s
where  m.memNo = s.memno
By the numbers量化數據量化数据

Overlap you can count 可量化的重疊 可量化的重叠

Files & overlap檔案與重疊文件与重叠
Metric指標指标 WSDSCUICAC
en/ business filesen/ 業務檔案en/ 业务文件130292
Shared root files共有根目錄檔案共有根目录文件8 shared
Shared en/ filenames共有 en/ 檔名共有 en/ 文件名22 shared
Authorial fingerprints作者指紋頻率作者指纹频率
Idiom寫法写法 WSDSCUICAC
Execute(TheString)4853
For Each Field…4530
month(date()) -4
deleted=-1
CheckUserStatus inc.
Similarity by subsystem逐子系統相似度逐子系统相似度 initial / after skeptic初評 / 覆核後初评 / 复核后
Lineage / version血緣 / 版本號血缘 / 版本号
92 / 92
Database schema資料庫結構数据库结构
90 / 90
Coding idioms編碼習慣 / 指紋编码习惯 / 指纹
90 / 90
Savings / accounting股金 / 會計股金 / 会计
88 / 88
Loan subsystem貸款子系統贷款子系统
85 / 85
Infrastructure / login基礎設施 / 登入基础设施 / 登录
78 / 68
UI / menu templatesUI / 選單模板UI / 菜单模板
72 / 62
Reports / printing報表 / 列印报表 / 打印
78 / 52

Scores shown are conservative: every dimension was re-checked by a skeptic agent instructed to argue "unrelated". The lower bars (reports, UI) reflect genuine divergence after years of separate maintenance — not weaker provenance. 此處分數偏保守:每個維度都由一個被要求論證「無關」的覆核員重新挑刺。較低的條目(報表、UI)反映的是多年獨立維護後的真實分化,而非血緣較弱。 此处分数偏保守:每个维度都由一个被要求论证「无关」的复核员重新挑刺。较低的条目(报表、UI)反映的是多年独立维护后的真实分化,而非血缘较弱。

Where they drifted分化之處分化之处

Same root, separate branches 同根異枝 同根异枝

Same origin does not mean identical today. After years of independent upkeep, the two forks diverged in six clear ways. 同源不代表今天完全一樣。經過多年獨立維護,兩個分支在六個方面明顯分化。 同源不代表今天完全一样。经过多年独立维护,两个分支在六个方面明显分化。

ICAC added a full GL layer廉署多了完整總帳層廉署多了完整总账层

17 gl_* account constants and a glTxListPrint*.asp report family — general-ledger accounting that WSDSCU has none of. 17 個 gl_* 科目常數與 glTxListPrint*.asp 報表系列——水務署完全沒有的總帳會計層。 17 个 gl_* 科目常量与 glTxListPrint*.asp 报表系列——水务署完全没有的总账会计层。

Bilingual split雙語目錄拆分双语目录拆分

ICAC keeps separate cn/ + en/ trees; WSDSCU has only en/ with Chinese mixed inline. 廉署分為 cn/en/ 兩套目錄;水務署只有 en/,中文混排在檔案內。 廉署分为 cn/en/ 两套目录;水务署只有 en/,中文混排在文件内。

WSDSCU lived longer水務署維護更久水务署维护更久

Maintained into 2021 with PDF/TXT export and IE10 hints; ICAC pages mostly sit in the IE5/IE8 era. 維護到 2021 年,新增 PDF/TXT 匯出與 IE10 提示;廉署頁面多停留在 IE5/IE8 時代。 维护到 2021 年,新增 PDF/TXT 导出与 IE10 提示;廉署页面多停留在 IE5/IE8 时代。

Transaction-code dialects交易代碼字典分化交易代码字典分化

The LoanCode/ShareCode case functions share structure but diverged: Chinese codes vs English mnemonics. LoanCode/ShareCode 的 case 函式結構相同,但內容分化:中文碼 vs 英文助記碼。 LoanCode/ShareCode 的 case 函数结构相同,但内容分化:中文码 vs 英文助记码。

Menu JS rewritten選單 JS 改寫菜单 JS 改写

activateMenu() (document.all) vs a timer-based mopen()/mclose() — the same menu, re-implemented. activateMenu()(document.all)vs 定時器式 mopen()/mclose()——同一套選單,重新實作。 activateMenu()(document.all)vs 定时器式 mopen()/mclose()——同一套菜单,重新实现。

Config differences設定差異配置差异

Different DB targets, session timeout 1200 vs 30, and charset/locale settings. 資料庫目標不同、會話逾時 1200 vs 30、字元集/地區設定不同。 数据库目标不同、会话超时 1200 vs 30、字符集/区域设置不同。

Lineage血緣關係血缘关系

Sibling forks of one product 同一產品的兄弟分支 同一产品的兄弟分支

Neither is cleanly the parent of the other. Both descend from one developer's "Credit Union" management product; each footer's version marks the base release it was forked from. 兩者並非簡單的「誰是誰的父版本」。它們都源自同一位開發者的「儲蓄互助社」管理產品;各自頁腳的版本號標示其 fork 時所基於的母產品版本。 两者并非简单的「谁是谁的父版本」。它们都源自同一位开发者的「储蓄互助社」管理产品;各自页脚的版本号标示其 fork 时所基于的母产品版本。

WSDSCU
Credit Union Ver 1.01.200604

Water Supplies Department branch · forked from the ~2006 base, then actively maintained through 2021 (PDF/TXT reports, IE10). 水務署分支 · 基於約 2006 年母版,之後一直積極維護至 2021 年(PDF/TXT 報表、IE10)。 水务署分支 · 基于约 2006 年母版,之后一直积极维护至 2021 年(PDF/TXT 报表、IE10)。

ICAC
Credur Union Ver 2.00.200804VER3.0i

ICAC branch · built on the ~2008 (v2) base and evolved toward v3, adding the rich GL accounting layer and a bilingual cn/ tree. 廉政公署分支 · 基於約 2008(v2)母版並向 v3 演進,新增完整總帳會計層與雙語 cn/ 目錄。 廉政公署分支 · 基于约 2008(v2)母版并向 v3 演进,新增完整总账会计层与双语 cn/ 目录。

Shared risks共用隱患共用隐患

Same DNA, same vulnerabilities 同源同病 同源同病

Because they share a codebase, both inherit the same high-risk patterns — worth noting for the migration. 因為同源,兩邊都帶著相同的高危寫法——遷移時值得注意。 因为同源,两边都带着相同的高危写法——迁移时值得注意。

SQL injection by string concatenation字串拼接造成 SQL 注入字符串拼接造成 SQL 注入

SQL is built everywhere as "… where username ='" & username & "'"到處用 "… where username ='" & username & "'" 拼接 SQL。到处用 "… where username ='" & username & "'" 拼接 SQL。

Dynamic Execute() of user inputExecute() 動態執行使用者輸入Execute() 动态执行用户输入

The Execute(TheString) auto-variable idiom runs attacker-influenced strings — variable / code injection risk.Execute(TheString) 自動變數寫法會執行受攻擊者影響的字串——存在變數/程式碼注入風險。Execute(TheString) 自动变量写法会执行受攻击者影响的字符串——存在变量/代码注入风险。

Hard-coded DB credentials硬編碼資料庫憑證硬编码数据库凭据

WSDSCU conn.asp ships a plaintext sa password (Pwd=Mountext666!). Rotate it and purge from the repo.水務署 conn.asp 明文寫了 sa 密碼(Pwd=Mountext666!)。請輪換並從版本庫清除。水务署 conn.asp 明文写了 sa 密码(Pwd=Mountext666!)。请轮换并从代码库清除。

How this was checked本報告如何得出本报告如何得出

Static analysis (grep / diff across both source trees) plus a structured 8-dimension comparison run by 18 parallel agents. Every finding was re-checked by an adversarial skeptic agent told to argue the systems are unrelated; the scores shown are the conservative post-skeptic numbers. No code was executed — provenance was judged from shared idioms, schema, templates and version strings. 靜態分析(對兩套原始碼樹做 grep / diff)加上由 18 個並行代理執行的結構化八維度比對。每項發現都由一個被要求論證系統「無關」的覆核代理重新檢驗;所示分數為保守的覆核後數值。全程不執行程式碼——血緣判斷依據共用的寫法、結構、模板與版本字串。 静态分析(对两套源码树做 grep / diff)加上由 18 个并行代理执行的结构化八维度比对。每项发现都由一个被要求论证系统「无关」的复核代理重新检验;所示分数为保守的复核后数值。全程不执行代码——血缘判断依据共用的写法、结构、模板与版本字符串。

Bottom line: one author, one codebase, two clients — WSD and ICAC. Highly similar; same origin, beyond reasonable doubt. 一句話總結:同一位作者、同一套底層程式碼、兩個客戶——水務署與廉政公署。高度相似;同源無疑。 一句话总结:同一位作者、同一套底层代码、两个客户——水务署与廉政公署。高度相似;同源无疑。

Part 2 · Migration Feasibility 第二部分 · 改造可行性 第二部分 · 改造可行性
Reuse the new system for ICAC 以新系統改造出廉署版 以新系统改造出廉署版

How hard is the ICAC build? 改造成廉署版有多難? 改造成廉署版有多难?

Because the two old systems are the same base, much of the new WSD system ports to ICAC. The new system is also further along than a glance suggests — a real backend already exists. The remaining difficulty is concentrated in three gaps. 因為兩套舊系統同源,新水務署系統有很大一部分可直接搬到廉署。而且新系統比乍看更成熟——後端是真實存在的。剩下的難度集中在三道缺口。 因为两套旧系统同源,新水务署系统有很大一部分可直接搬到廉署。而且新系统比乍看更成熟——后端是真实存在的。剩下的难度集中在三道缺口。

THE NEW SYSTEM'S REAL FOUNDATION (not a stub) 新系統的真實底座(不是樁) 新系统的真实底座(不是桩)

Vue 3
Frontend · Pinia · Tailwind前端 · Pinia · Tailwind前端 · Pinia · Tailwind
Django · DRF
Real REST backend真實 REST 後端真实 REST 后端
PostgreSQL
Accounting-grade RDBMS會計級關係資料庫会计级关系数据库
167
API routes in web/urls.pyweb/urls.py API 路由web/urls.py API 路由
~46.5k
Backend lines of code後端程式碼行數后端代码行数
OTP · 2FA
Auth already built鑑權已建鉴权已建

The finance app already models the whole WSDSCU domain (loan / share / autopay / dividend / bankruptcy / IVA / month-end). The 13-line server/index.js is just a Cloudflare placeholder — the real backend is the Django project under Mutual-Aid-Societ-App-Demo/backend. finance 應用已經把整套 WSDSCU 業務建模(貸款/股金/自動轉賬/股息/破產/IVA/月結)。那 13 行的 server/index.js 只是 Cloudflare 佔位——真正的後端是 Mutual-Aid-Societ-App-Demo/backend 下的 Django 專案。 finance 应用已经把整套 WSDSCU 业务建模(贷款/股金/自动转账/股息/破产/IVA/月结)。那 13 行的 server/index.js 只是 Cloudflare 占位——真正的后端是 Mutual-Aid-Societ-App-Demo/backend 下的 Django 项目。

Ports over (the same-origin dividend)可直接複用(同源紅利)可直接复用(同源红利)

  • Member / loan / share / savings / autopay / debt main flows社員/貸款/股金/儲蓄/自動轉賬/清數主線社员/贷款/股金/储蓄/自动转账/清数主线
  • Monthly account-statistics reports (cash / bank / treasury / other)每月帳統計報表(現金/銀行/庫房/其他)每月账统计报表(现金/银行/库房/其他)
  • PMT / fiscal-year logic, member schema, OTP auth, permissionsPMT/財年邏輯、社員結構、OTP 鑑權、權限PMT/财年逻辑、社员结构、OTP 鉴权、权限

Must be added for ICAC廉署需新增廉署需新增

  • GL general ledger — the one truly net-new subsystemGL 總帳——唯一真·淨新增的子系統GL 总账——唯一真·净新增的子系统
  • Bilingual EN + 中 — no i18n framework today中英雙語——目前沒有 i18n 框架中英双语——目前没有 i18n 框架
  • Data migration + reconciliation to the cent; transaction-code dialect; multi-tenant config數據遷移+對賬分文對平;交易碼方言;多租戶配置数据迁移+对账分文对平;交易码方言;多租户配置
Effort by work package分工作包工作量分工作包工作量 solo hand-coded → with Claude Code傳統單人 → 用 Claude Code传统单人 → 用 Claude Code
Work package工作包工作包 Solo (p-mo)傳統(人月)传统(人月) + AI (p-mo)+AI(人月)+AI(人月) Risk風險风险
Bilingual i18n中英雙語 i18n中英双语 i18n3–3.50.5–1Med
Transaction-code dialect config交易碼方言配置交易码方言配置1–1.50.4–0.6Med-High中高中高
Multi-tenant config多租戶配置化多租户配置化1–1.50.5–0.8Med
GLI group-life insuranceGLI 團體保險GLI 团体保险1.50.5–0.8Med
GL general ledgerGL 總帳子系統GL 总账子系统4–61.5–2.5High
Data migration + tie-out數據遷移+對賬数据迁移+对账2–3.51.5–2.5High
Extra report variants額外報表變體额外报表变体10.3–0.5Low
Testing / UAT / parallel-run測試/UAT/並行運行测试/UAT/并行运行1.5–21–1.5High
Total合計合计15–22~6–10

Claude Code compresses the writing to a few months — but it cannot compress proving the books are correct. The bottleneck shifts to GL posting-rule review and a financial parallel-run, not to typing code. Claude Code 把「寫代碼」壓到幾個月——但壓不動「證明賬是對的」。瓶頸轉移到 GL 過賬規則核對與財務並行試運行,而不是敲代碼。 Claude Code 把「写代码」压到几个月——但压不动「证明账是对的」。瓶颈转移到 GL 过账规则核对与财务并行试运行,而不是敲代码。

6–10
person-months effective effort (AI-assisted)人月·有效工時(AI 輔助)人月·有效工时(AI 辅助)
4–7
months calendar to safe go-live (gated by parallel-run)月·到安全上線(受並行運行制約)月·到安全上线(受并行运行制约)
≠ 1.8 yr
not the hand-coded single-dev estimate不是傳統單人手寫的估算不是传统单人手写的估算
Part 3 · Missing Module & Mock Data 第三部分 · 缺失模組與 Mock 數據 第三部分 · 缺失模块与 Mock 数据
What GL actually looks like GL 總帳長什麼樣 GL 总账长什么样

The general ledger, in mock data 用 Mock 數據看懂總帳 用 Mock 数据看懂总账

"Monthly account statistics" (which both old systems and the new system have) sums the member sub-ledgers. The general ledger (which only ICAC has) is a separate double-entry book: every transaction posts balanced debit/credit entries that must foot to a trial balance. Below is mock data so you can see exactly what that means. 「每月帳統計」(兩套舊系統與新系統都有)是把社員子帳求和。總帳(只有廉署有)是另一本複式帳:每筆交易都過借貸平衡的分錄,並必須匯總出試算平衡。下面用 Mock 數據讓你直觀看懂。 「每月账统计」(两套旧系统与新系统都有)是把社员子账求和。总账(只有廉署有)是另一本复式账:每笔交易都过借贷平衡的分录,并必须汇总出试算平衡。下面用 Mock 数据让你直观看懂。

Statistics report vs. general ledger統計報表 vs. 總帳统计报表 vs. 总账
Monthly statistics (have)每月帳統計(已有)每月账统计(已有) GL ledger (missing)GL 總帳(缺失)GL 总账(缺失)
Data source數據來源数据来源SUM the share/loan sub-ledgersshare/loan 子帳求和share/loan 子账求和A separate voucher table glTx獨立憑證表 glTx独立凭证表 glTx
Core action核心動作核心动作Totals & counts by code按代碼求和、計數按代码求和、计数Balanced debit/credit posting借貸平衡過賬借贷平衡过账
Chart of accounts會計科目表会计科目表✗ none✗ 沒有✗ 没有✓ 14 gl_ accounts✓ 14 個 gl_ 科目✓ 14 个 gl_ 科目
Debit = Credit?借=貸?借=贷?✗ N/A✗ 不涉及✗ 不涉及✓ must balance✓ 必須對平✓ 必须对平
Trial balance / P&L試算平衡/損益试算平衡/损益✗ none✗ 沒有✗ 没有glTxList-Bal/WBalglTxList-Bal/WBalglTxList-Bal/WBal
For whom給誰看给谁看Management (business figures)管理層(經營數字)管理层(经营数字)Auditors (do the books foot?)核數師(賬平不平)核数师(账平不平)

MOCK 1 · Chart of accounts (the 14 ICAC gl_ accounts) MOCK 1 · 會計科目表(廉署 14 個 gl_ 科目) MOCK 1 · 会计科目表(廉署 14 个 gl_ 科目)

Code代碼代码 Account科目科目 Type類型类型
101Loans Receivable貸款應收贷款应收Asset資產资产
104Bank銀行存款银行存款Asset資產资产
110Investments投資投资Asset資產资产
204Interest Rebate利息回扣利息回扣Liability負債负债
209Subsidy補貼补贴Liability負債负债
302Loan Insurance貸款保險贷款保险Liability負債负债
306Insurance保險保险Liability負債负债
310Members' Shares社員股金社员股金Equity權益权益
313Profit & Loss損益损益P&L損益损益
315Group Life Insurance團體人壽保險团体人寿保险Liability負債负债
401Interest Income利息收入利息收入Income收入收入
405Investment Income投資收益投资收益Income收入收入
409Other Income其他收入其他收入Income收入收入
410Insurance Surplus保險盈餘保险盈余Income收入收入

MOCK 2 · Journal vouchers — every transaction posts a balanced entry MOCK 2 · 記賬憑證——每筆交易都過一張借貸平衡分錄 MOCK 2 · 记账凭证——每笔交易都过一张借贷平衡分录

Vno 2406-0087 2024-06-15 · M00231 · cash loan repayment現金還款现金还款
104Bank銀行存款银行存款Dr 1,000.00
101Loans Receivable貸款應收贷款应收Cr 900.00
401Interest Income利息收入利息收入Cr 100.00
Dr 1,000.00 = Cr 1,000.00借 1,000.00 = 貸 1,000.00借 1,000.00 = 贷 1,000.00
Vno 2406-0088 2024-06-15 · M00231 · share deposit存入股金存入股金
104Bank銀行存款银行存款Dr 500.00
310Members' Shares社員股金社员股金Cr 500.00
Dr 500.00 = Cr 500.00借 500.00 = 貸 500.00借 500.00 = 贷 500.00

MOCK 3 · Trial balance — the whole entity must foot MOCK 3 · 試算平衡表——整盤帳必須對平 MOCK 3 · 试算平衡表——整盘账必须对平

Account科目科目 Dr Cr
101 Loans Receivable貸款應收贷款应收12,480,000.00
104 Bank銀行存款银行存款8,650,000.00
110 Investments投資投资5,000,000.00
204 Interest Rebate利息回扣利息回扣120,000.00
209 Subsidy補貼补贴80,000.00
310 Members' Shares社員股金社员股金24,300,000.00
313 Profit & Loss損益损益270,000.00
401 Interest Income利息收入利息收入1,150,000.00
405 Investment Income投資收益投资收益210,000.00
Total合計合计26,130,000.0026,130,000.00
Total Dr = Total Cr — the books foot借方合計 = 貸方合計 — 整盤帳對平借方合计 = 贷方合计 — 整盘账对平

MOCK 4 · Posting map — transaction → debit / credit accounts MOCK 4 · 過賬對照表——交易 → 借/貸科目 MOCK 4 · 过账对照表——交易 → 借/贷科目

Transaction交易交易 Dr Cr
New loan disbursed新貸款發放新贷款发放101 Loans Recv.貸款應收贷款应收104 Bank銀行银行
Cash repay · principal現金還款·本金现金还款·本金104 Bank銀行银行101 Loans Recv.貸款應收贷款应收
Cash repay · interest現金還款·利息现金还款·利息104 Bank銀行银行401 Interest Inc.利息收入利息收入
Share deposit存入股金存入股金104 Bank銀行银行310 Shares股金股金
Dividend paid派發股息派发股息313 P&L損益损益310 Shares股金股金
Bad-debt write-off壞賬核銷坏账核销313 P&L損益损益101 Loans Recv.貸款應收贷款应收
GLI premium deduction團體保險扣費团体保险扣费310 Shares股金股金315 Group Life Ins.團體人壽团体人寿

⚠ Illustrative mappings — the real debit/credit rules must be confirmed against ICAC's actual code (its gl_ constants and posting files). Getting a direction wrong silently mis-posts money. ⚠ 此為示意對照——真實借貸規則須按 ICAC 實際代碼(其 gl_ 常量與過賬檔案)核對。方向錯了會靜默錯賬。 ⚠ 此为示意对照——真实借贷规则须按 ICAC 实际代码(其 gl_ 常量与过账文件)核对。方向错了会静默错账。

MOCK 5 · The data model — a GL voucher as JSON (for the new Django/REST backend) MOCK 5 · 數據模型——GL 憑證的 JSON(給新 Django/REST 後端) MOCK 5 · 数据模型——GL 凭证的 JSON(给新 Django/REST 后端)

GLVoucher · POST /api/web/gl/vouchers/
1234567891011121314
{
  "voucher_no": "2406-0087",
  "date": "2024-06-15", "fiscal_year": 2024,
  "source": "loan_repayment",   // posted from the sub-ledger
  "member_no": "M00231",
  "entries": [
    { "account": "104", "name": "Bank",            "dr": 1000.00, "cr": 0 },
    { "account": "101", "name": "Loans Receivable", "dr": 0, "cr": 900.00 },
    { "account": "401", "name": "Interest Income",  "dr": 0, "cr": 100.00 }
  ],
  "total_dr": 1000.00, "total_cr": 1000.00,
  "balanced": true           // must be true before posting
}

In the new backend this would be a GLVoucher + GLEntry model pair, written atomically with the sub-ledger row inside one DB transaction via a shared post_to_gl() hook on the existing finance methods. 在新後端裡,這會是一對 GLVoucher + GLEntry 模型,透過掛在現有 finance 方法上的 post_to_gl() 鉤子,與子帳行在同一個資料庫事務中原子寫入。 在新后端里,这会是一对 GLVoucher + GLEntry 模型,通过挂在现有 finance 方法上的 post_to_gl() 钩子,与子账行在同一个数据库事务中原子写入。

The other missing module: GLI group-life insurance另一個缺失模組:GLI 團體人壽保險另一个缺失模块:GLI 团体人寿保险

~13 ICAC screens: members join / quit / adjust a group-life plan; each premium deduction posts to the GL too. A mock enrolment record: 廉署約 13 個畫面:社員加入/退出/調整團體人壽計劃;每次扣保費也要過 GL。一條投保 Mock 記錄: 廉署约 13 个画面:社员加入/退出/调整团体人寿计划;每次扣保费也要过 GL。一条投保 Mock 记录:

GLIEnrolment
1234
{ "member_no": "M00231", "plan": "GLI-A", "sum_assured": 200000,
  "annual_premium": 480.00, "subsidy": 120.00, "net_premium": 360.00,
  "status": "active", "joined": "2019-08-01" }
// each deduction posts: Dr 310 Shares / Cr 315 Group Life Ins.
For Leadership 給管理層 给管理层
Executive Summary 執行摘要 执行摘要

The situation, in plain terms 用大白話講清楚現況 用大白话讲清楚现况

One sentence: rebuilding the ICAC system on our new platform is technically feasible and the risk is manageable. Because we already have a working foundation and AI-assisted development, this is a project measured in months — not the year or two people might assume. 一句話:把廉政公署的舊系統改造到我們的新平台上,技術上完全可行、風險可控。因為我們已有可運作的基礎、又有 AI 輔助開發,這是一個以「幾個月」計、而非外界以為的「一兩年」的專案。 一句话:把廉政公署的旧系统改造到我们的新平台上,技术上完全可行、风险可控。因为我们已有可运作的基础、又有 AI 辅助开发,这是一个以「几个月」计、而非外界以为的「一两年」的项目。

1What we investigated我們做了什麼調查我们做了什么调查

We compared the Independent Commission Against Corruption (ICAC) old system with the Water Supplies Department (WSD) old system we are already modernising, and we assessed how hard it would be to adapt our new system for ICAC. 我們把廉政公署的舊系統,與我們正在改造的水務署舊系統做了對比,並評估了把現有新系統改造給廉政公署使用的難度。 我们把廉政公署的旧系统,与我们正在改造的水务署旧系统做了对比,并评估了把现有新系统改造给廉政公署使用的难度。

2Finding 1 — the two old systems came from the same hand發現一:兩套舊系統師出同門发现一:两套旧系统师出同门

ICAC's old system and WSD's old system are essentially two versions built by the same person on the same foundation. That means most of the new system we are building for WSD can be reused for ICAC, instead of starting from scratch. This is the biggest piece of good news — it saves money and time. 廉政公署的舊系統,和水務署的舊系統,基本上是同一個人、用同一套底子做出來的兩個版本。這意味著我們為水務署做的新系統,大部分可以直接沿用到廉政公署,不必從零開始。這是最大的好消息——省錢、省時間。 廉政公署的旧系统,和水务署的旧系统,基本上是同一个人、用同一套底子做出来的两个版本。这意味着我们为水务署做的新系统,大部分可以直接沿用到廉政公署,不必从零开始。这是最大的好消息——省钱、省时间。

3Finding 2 — our new system's foundation is already built發現二:我們新系統的底子已經打好发现二:我们新系统的底子已经打好

The core of our new WSD system — member management, loans, shares, automatic transfers and the day-to-day business — is already built and working. It is not an empty shell. ICAC can sit directly on top of this. 我們給水務署做的新系統,核心部分(社員管理、貸款、股金、自動轉賬等日常業務)已經建好並能運作,不是空架子。廉政公署可以直接建在它之上。 我们给水务署做的新系统,核心部分(社员管理、贷款、股金、自动转账等日常业务)已经建好并能运转,不是空架子。廉政公署可以直接建在它之上。

4Finding 3 — ICAC has one extra piece: a full set of accounting books發現三:廉政公署多了一塊「正規會計帳」发现三:廉政公署多了一块「正规会计账」

ICAC additionally keeps a complete set of accounting books — recording the whole organisation's income and spending, and producing the financial statements an auditor checks. Neither WSD nor our new system has this, so it must be built from scratch. It is the largest part of this project and the part that most needs professional oversight. On top of it we also need a bilingual Chinese/English interface, and we must move the old historical data into the new system and check every figure ties up exactly. 廉政公署額外有一整套完整的會計帳——記錄整個機構的收支,並能產生給核數師查核的財務報表。這是水務署和我們新系統都沒有的,必須從頭建。它是本次改造裡最大、也最需要專業把關的一塊。此外還要做中英文雙語介面,並把舊系統的歷史資料搬到新系統、逐筆核對帳目分毫不差。 廉政公署额外有一整套完整的会计账——记录整个机构的收支,并能生成给核数师查核的财务报表。这是水务署和我们新系统都没有的,必须从头建。它是本次改造里最大、也最需要专业把关的一块。此外还要做中英文双语界面,并把旧系统的历史数据搬到新系统、逐笔核对账目分毫不差。

5How long, and where the difficulty is要花多久、難在哪要花多久、难在哪

Because we develop with AI assistance, writing the software is far faster than the traditional pace. But why does going live still take longer than the build? Because an accounting system must run alongside the old system for several months and be reconciled month by month with zero discrepancy before we can switch over — that waiting and checking cannot be sped up by AI; it is a necessary safety step. This is nowhere near the year or two an outside estimate would assume. 因為我們有 AI 輔助開發,寫程式的速度比傳統快很多。但為什麼上線還是比開發久?因為帳務系統必須與舊系統並行運作數個月、逐月核對完全無誤,才能正式切換——這段等待與核對靠 AI 也壓縮不了,是穩妥起見的必要步驟。這遠不是外界傳統估算的一兩年。 因为我们有 AI 辅助开发,写程序的速度比传统快很多。但为什么上线还是比开发久?因为账务系统必须与旧系统并行运行几个月、逐月核对完全无误,才能正式切换——这段等待与核对靠 AI 也压缩不了,是稳妥起见的必要步骤。这远不是外界传统估算的一两年。

6–10
person-months of actual build effort (roughly one person ~half a year; faster with a small team)人月·實際開發投入(約一人全職半年多;小團隊更快)人月·实际开发投入(约一人全职半年多;小团队更快)
4–7
months to a safe go-live (includes running old & new side by side)月·到安全上線(含新舊並行核對)月·到安全上线(含新旧并行核对)
not 1–2 yrs非一兩年非一两年
far below the traditional hand-built estimate遠低於傳統手工估算远低于传统手工估算

6The biggest risk最大的風險最大的风险

The money and the books cannot be wrong — not by a single cent. The accounting and the data transfer must be exact. ICAC in particular is held to very high oversight and audit standards, so any error in the books is a serious matter. The real challenge of this project is therefore not "writing it fast", but "getting the books right and making them stand up to an audit". 錢和帳,一分都不能錯。會計帳目與資料搬遷必須分毫不差。尤其廉政公署受極高的監管與審計標準約束,帳目出一點差錯都是大事。因此這個專案真正的難點不在「寫得快」,而在「帳要對、要經得起審計」。 钱和账,一分都不能错。会计账目与数据搬迁必须分毫不差。尤其廉政公署受极高的监管与审计标准约束,账目出一点差错都是大事。因此这个项目真正的难点不在「写得快」,而在「账要对、要经得起审计」。

7Our recommendations我們的建議我们的建议

(1) Run both WSD and ICAC on one system, configured per organisation, so we never end up maintaining two systems that drift apart. (2) Confirm two things early: whether ICAC definitely requires an English interface (it directly affects the workload), and get access to the real old data for checking as soon as possible. (3) Put our strongest people on the accounting books and the data reconciliation — that is where the project is won or lost. (1)讓水務署與廉政公署共用一套系統、按機構分別配置,避免日後維護兩套各自走樣。(2)儘早確認兩件事:廉政公署是否一定要英文介面(直接影響工作量),並儘快取得舊系統的真實資料以供核對。(3)把最強的人力放在「會計帳目」與「資料核對」這兩件事上——成敗在此。 (1)让水务署与廉政公署共用一套系统、按机构分别配置,避免日后维护两套各自走样。(2)尽早确认两件事:廉政公署是否一定要英文界面(直接影响工作量),并尽快取得旧系统的真实数据以供核对。(3)把最强的人力放在「会计账目」与「数据核对」这两件事上——成败在此。

Bottom line for leadership: this can be done, is worth doing, and is not slow — thanks to reusing same-origin work and AI-assisted development. The one thing to keep a close eye on is making sure the financial books are accurate and audit-proof. 給領導的結論:這件事能做、值得做、且不慢——靠的是同源代碼的複用與 AI 輔助開發。唯一要重點盯住的,是確保財務帳目準確、經得起審計。 给领导的结论:这件事能做、值得做、且不慢——靠的是同源代码的复用与 AI 辅助开发。唯一要重点盯住的,是确保财务账目准确、经得起审计。